From 8eb7269f27c9a8634c4c73f3be1664755740527a Mon Sep 17 00:00:00 2001 From: hreyes Date: Fri, 24 Apr 2026 08:55:37 -0600 Subject: [PATCH 1/8] feature/bitacora-filtros-alineacion --- .../dashboard/audit_logs/bitacora-tab.svelte | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/frontend/src/routes/dashboard/audit_logs/bitacora-tab.svelte b/frontend/src/routes/dashboard/audit_logs/bitacora-tab.svelte index 4b02f35a..407b76db 100644 --- a/frontend/src/routes/dashboard/audit_logs/bitacora-tab.svelte +++ b/frontend/src/routes/dashboard/audit_logs/bitacora-tab.svelte @@ -170,13 +170,13 @@
-
-
+
+
{text('Bitácora de Auditoría', 'Audit Log')} {text(`Mostrando ${logs.length} de ${total} registros`, `Showing ${logs.length} of ${total} records`)}
-
-
+
+
@@ -197,7 +197,7 @@ id="procedure" bind:value={procedureFilter} onchange={handleFilterChange} - class="flex h-9 w-[180px] rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none" + class="flex h-9 w-[180px] shrink-0 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none" title={text('Procedimiento', 'Procedure')} > @@ -208,27 +208,27 @@ - - + - -
-
-
- -
-
From bbe0b5c02480ccb87c34d9cd29752196755f3e6c Mon Sep 17 00:00:00 2001 From: hreyes Date: Fri, 24 Apr 2026 09:30:35 -0600 Subject: [PATCH 3/8] feature/sidebar-empresa-refresh --- .../general_catalogs/company/create-edit-dialog.svelte | 2 ++ frontend/src/routes/dashboard/+layout.svelte | 7 +++++-- .../general_catalogs/company_information/+page.svelte | 2 ++ .../company_information/edit/[[id]]/+page.svelte | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/components/dashboard/general_catalogs/company/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/general_catalogs/company/create-edit-dialog.svelte index 8a37a99c..933f4680 100644 --- a/frontend/src/lib/components/dashboard/general_catalogs/company/create-edit-dialog.svelte +++ b/frontend/src/lib/components/dashboard/general_catalogs/company/create-edit-dialog.svelte @@ -6,6 +6,7 @@ import * as Tabs from "$lib/components/ui/tabs"; import { Switch } from "$lib/components/ui/switch"; import { createCompany, updateCompany, type Company } from "$lib/api/dashboard/a76/general_catalogs/company"; + import { companyStore } from "$lib/stores/company.svelte"; let { open = $bindable(false), @@ -140,6 +141,7 @@ throw new Error(response.error); } + await companyStore.loadCompanies(); open = false; if (onSuccess) onSuccess(); } catch (e) { diff --git a/frontend/src/routes/dashboard/+layout.svelte b/frontend/src/routes/dashboard/+layout.svelte index 2e7cbafe..2da2dfd1 100644 --- a/frontend/src/routes/dashboard/+layout.svelte +++ b/frontend/src/routes/dashboard/+layout.svelte @@ -1,6 +1,6 @@ - - diff --git a/frontend/src/routes/dashboard/general_catalogs/tariff-fractions/american/+page.ts b/frontend/src/routes/dashboard/general_catalogs/tariff-fractions/american/+page.ts new file mode 100644 index 00000000..b2bd1df9 --- /dev/null +++ b/frontend/src/routes/dashboard/general_catalogs/tariff-fractions/american/+page.ts @@ -0,0 +1,7 @@ +import { redirect } from '@sveltejs/kit'; +import type { PageLoad } from './$types'; + +/** Legacy URL; catalog is unified under SITAR US listing. */ +export const load: PageLoad = () => { + throw redirect(308, '/dashboard/general_catalogs/tariff-fractions/us'); +}; From 882d754d33d83a782be052b909c0a8dbfdc0dc46 Mon Sep 17 00:00:00 2001 From: hreyes Date: Fri, 24 Apr 2026 10:28:52 -0600 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20persistir=20selecci=C3=B3n=20de=20c?= =?UTF-8?q?ompa=C3=B1=C3=ADa=20mediante=20cookies=20y=20SSR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/lib/stores/company.svelte.ts | 21 +++++++++++++------ .../src/routes/dashboard/+layout.server.ts | 4 ++++ frontend/src/routes/dashboard/+layout.svelte | 2 +- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/frontend/src/lib/stores/company.svelte.ts b/frontend/src/lib/stores/company.svelte.ts index ddcbf4b9..833d2ab0 100644 --- a/frontend/src/lib/stores/company.svelte.ts +++ b/frontend/src/lib/stores/company.svelte.ts @@ -35,7 +35,7 @@ class CompanyStore { * Carga las compañías del tenant del usuario desde el backend * @param preloadedCompanies - Compañías pre-cargadas desde el servidor (SSR) */ - async loadCompanies(preloadedCompanies?: Company[]) { + async loadCompanies(preloadedCompanies?: Company[], activeCompanyId?: number) { // Si tenemos compañías pre-cargadas, usarlas directamente if (preloadedCompanies && preloadedCompanies.length > 0) { // Detectar si el tenant ha cambiado @@ -49,9 +49,18 @@ class CompanyStore { this._currentTenantId = newTenantId; this._companies = preloadedCompanies; - // Si hay compañías y no hay una activa, seleccionar la primera o la guardada + // Si hay compañías y no hay una activa, seleccionar la correcta if (this._companies.length > 0 && !this._activeCompany) { - // Intentar restaurar la compañía guardada + // Prioridad 1: ID pasado por parámetro (desde SSR/Cookie) + if (activeCompanyId) { + const company = this._companies.find(c => c.id === activeCompanyId); + if (company) { + this.setActiveCompany(company, true); + return; + } + } + + // Prioridad 2: Intentar restaurar de localStorage (Browser only) if (typeof window !== 'undefined') { const savedId = localStorage.getItem('activeCompanyId'); if (savedId) { @@ -62,7 +71,7 @@ class CompanyStore { } } } - // Si no hay guardada, seleccionar la primera + // Prioridad 3: Si no hay nada, seleccionar la primera this.setActiveCompany(this._companies[0], true); // silent=true para inicialización } return; @@ -219,8 +228,8 @@ class CompanyStore { * Inicializa el store cargando las compañías * @param preloadedCompanies - Compañías pre-cargadas desde el servidor (SSR) */ - async initialize(preloadedCompanies?: Company[]) { - await this.loadCompanies(preloadedCompanies); + async initialize(preloadedCompanies?: Company[], activeCompanyId?: number) { + await this.loadCompanies(preloadedCompanies, activeCompanyId); // Si no hay compañías pre-cargadas, intentar restaurar de localStorage if (!preloadedCompanies) { this.restoreActiveCompany(); diff --git a/frontend/src/routes/dashboard/+layout.server.ts b/frontend/src/routes/dashboard/+layout.server.ts index efbab4ed..159b309c 100644 --- a/frontend/src/routes/dashboard/+layout.server.ts +++ b/frontend/src/routes/dashboard/+layout.server.ts @@ -45,10 +45,14 @@ export const load: LayoutServerLoad = async ({ cookies, url, fetch }) => { // No bloquear el dashboard si falla la carga de tenants } + // Obtener la compañía activa de la cookie para persistencia + const activeCompanyId = cookies.get('active_company_id'); + return { authenticated: true, user: { ...userData, token: accessToken }, companies, + activeCompanyId: activeCompanyId ? parseInt(activeCompanyId) : undefined, userTenants, error: undefined }; diff --git a/frontend/src/routes/dashboard/+layout.svelte b/frontend/src/routes/dashboard/+layout.svelte index 2e7cbafe..4294fe73 100644 --- a/frontend/src/routes/dashboard/+layout.svelte +++ b/frontend/src/routes/dashboard/+layout.svelte @@ -96,7 +96,7 @@ // ── Inicializar compañías ───────────────────────────────────────────── if (data.companies) { - companyStore.initialize(data.companies); + companyStore.initialize(data.companies, data.activeCompanyId); } // ── Escuchar cambios de compañía y recargar datos ───────────────────── From cbe80953b33fd4496843393b367158056388f867 Mon Sep 17 00:00:00 2001 From: hreyes Date: Fri, 24 Apr 2026 10:49:35 -0600 Subject: [PATCH 6/8] fix/parte-pais-no-guarda --- backend/api/v1/modules/a76/parts/service.py | 2 + .../dashboard/goods/parts/partForm.svelte | 44 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/backend/api/v1/modules/a76/parts/service.py b/backend/api/v1/modules/a76/parts/service.py index 16b06872..e6073ebb 100644 --- a/backend/api/v1/modules/a76/parts/service.py +++ b/backend/api/v1/modules/a76/parts/service.py @@ -138,6 +138,7 @@ class PartService: def create(cls, db: Session, part_data: PartCreateDTO, tenant_id: int, company_id: int) -> Part: # 1. Preparar datos data = part_data.model_dump() + print(f"DEBUG: PartService.create - full data: {data}") # Separar datos anidados fa_dict = data.pop('fa_data', None) @@ -316,6 +317,7 @@ class PartService: @classmethod def update(cls, db: Session, part_id: int, tenant_id: int, part_data: PartUpdateDTO, company_id: int) -> Optional[Part]: + print(f"DEBUG: PartService.update - part_id={part_id}, data={part_data.model_dump(exclude_unset=True)}") db_part = PartService.get_by_id(db, part_id, tenant_id, company_id) if not db_part: return None diff --git a/frontend/src/lib/components/dashboard/goods/parts/partForm.svelte b/frontend/src/lib/components/dashboard/goods/parts/partForm.svelte index 76b214ba..ab28cdb3 100644 --- a/frontend/src/lib/components/dashboard/goods/parts/partForm.svelte +++ b/frontend/src/lib/components/dashboard/goods/parts/partForm.svelte @@ -56,6 +56,7 @@ import { clientsProvidersApi } from '$lib/api/dashboard/a76/clients-providers'; import { classesApi } from '$lib/api/dashboard/a76/classes'; import { materialTypesApi } from '$lib/api/dashboard/a76/material-types'; + import { countriesApi } from '$lib/api/dashboard/reference_data/countries'; // Modales import { @@ -640,6 +641,7 @@ if (d.part_class) await fetchClassDesc(d.part_class, companyId); if (d.inv_data?.material_type) await fetchMaterialName(d.inv_data.material_type); await fetchSectorDesc(formData.sector); + if (formData.origin_country) await fetchCountryName(formData.origin_country); } } catch (e) { console.error(e); @@ -714,6 +716,19 @@ } } + async function fetchCountryName(code: string) { + selectedCountryName = ''; + if (!code) return; + try { + const res = await countriesApi.get(code); + if (res.data) { + selectedCountryName = res.data.description_es || res.data.description_en || ''; + } + } catch (e) { + console.error('Error fetching country name', e); + } + } + // --- HANDLERS --- function handleClientSelect(client: any) { if (modalContext === 'non_discharge') { @@ -1808,6 +1823,35 @@ >
+ +
+ +
+
+ + (showCountryModal = true)} + onkeydown={(event) => + openOnEnterOrSpace(event, () => (showCountryModal = true))} + tabindex="0" + class="cursor-pointer pl-9 transition-colors hover:bg-muted/50" + placeholder="Seleccione..." + /> +
+ +
+
Date: Fri, 24 Apr 2026 11:03:22 -0600 Subject: [PATCH 7/8] fix/agente-aduanal-inputs --- .../dashboard/customs_brokers/edit/[[id]]/+page.svelte | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/routes/dashboard/customs_brokers/edit/[[id]]/+page.svelte b/frontend/src/routes/dashboard/customs_brokers/edit/[[id]]/+page.svelte index f08dcc61..e9fd91be 100644 --- a/frontend/src/routes/dashboard/customs_brokers/edit/[[id]]/+page.svelte +++ b/frontend/src/routes/dashboard/customs_brokers/edit/[[id]]/+page.svelte @@ -466,8 +466,8 @@ : 'Selecciona un tipo...'} - Agente Aduanal Mexicano - Agente Aduanal Americano (Broker) + Agente Aduanal Mexicano + Agente Aduanal Americano (Broker)
@@ -851,9 +851,9 @@ {vuData.vu_figure_type || 'Seleccionar tipo de figura'} - AGENTE ADUANAL - APODERADO ADUANAL - MANDATARIO + AGENTE ADUANAL + APODERADO ADUANAL + MANDATARIO
From 5fbe6e1bbc0a1b7af4d6bacb90c378503f5d4452 Mon Sep 17 00:00:00 2001 From: hreyes Date: Fri, 24 Apr 2026 12:42:34 -0600 Subject: [PATCH 8/8] feature/fraccion-arancelaria-nico --- .../dashboard/goods/classes/columns.ts | 10 ++- .../classes/forms/FixedAssetClassForm.svelte | 28 +++++-- .../goods/fractions/TariffFractionList.svelte | 14 +++- .../modales/TariffFractionSelector.svelte | 10 ++- .../dashboard/goods/parts/columns.ts | 5 +- .../dashboard/goods/parts/partForm.svelte | 83 +++++++++++++++---- .../invoices/edit/items/fa/main-data.svelte | 40 +++------ .../edit/items/inv/item-sheet-inv.svelte | 43 ++++++---- .../lib/utils/mexican-tariff-fraction.test.ts | 67 +++++++++++++++ .../src/lib/utils/mexican-tariff-fraction.ts | 64 ++++++++++++++ .../goods/fixed-asset-classes/+page.svelte | 19 +++-- .../routes/dashboard/goods/parts/+page.svelte | 3 +- 12 files changed, 304 insertions(+), 82 deletions(-) create mode 100644 frontend/src/lib/utils/mexican-tariff-fraction.test.ts create mode 100644 frontend/src/lib/utils/mexican-tariff-fraction.ts diff --git a/frontend/src/lib/components/dashboard/goods/classes/columns.ts b/frontend/src/lib/components/dashboard/goods/classes/columns.ts index 44e390a9..68054536 100644 --- a/frontend/src/lib/components/dashboard/goods/classes/columns.ts +++ b/frontend/src/lib/components/dashboard/goods/classes/columns.ts @@ -2,6 +2,10 @@ import type { ColumnDef } from "@tanstack/table-core"; import { renderSnippet } from "$lib/components/ui/data-table/index.js"; import { createRawSnippet } from "svelte"; import type { A76Class } from "$lib/api/dashboard/a76/classes"; +import { + formatMexTariffDigitsForDisplay, + normalizeMexTariffDigitsStored +} from "$lib/utils/mexican-tariff-fraction"; export function createColumns(): ColumnDef[] { return [ @@ -135,7 +139,11 @@ export function createColumns(): ColumnDef[] { render: () => `${fr || ''}` }; }); - return renderSnippet(fracSnippet, { fr: row.original.fraction }); + const ext = row.original as A76Class & { import_tariff_code?: string | null }; + const fr = formatMexTariffDigitsForDisplay( + normalizeMexTariffDigitsStored(ext.import_tariff_code || ext.fraction || '') + ); + return renderSnippet(fracSnippet, { fr }); } }, { diff --git a/frontend/src/lib/components/dashboard/goods/classes/forms/FixedAssetClassForm.svelte b/frontend/src/lib/components/dashboard/goods/classes/forms/FixedAssetClassForm.svelte index 6c16bed8..33e12e95 100644 --- a/frontend/src/lib/components/dashboard/goods/classes/forms/FixedAssetClassForm.svelte +++ b/frontend/src/lib/components/dashboard/goods/classes/forms/FixedAssetClassForm.svelte @@ -14,6 +14,11 @@ getTariffFractions, type TariffFraction } from '$lib/api/dashboard/a76/general_catalogs/tariff-fractions'; + import { + buildMexTariffDigitsFromCatalogRow, + formatMexTariffDigitsForDisplay, + normalizeMexTariffDigitsStored + } from '$lib/utils/mexican-tariff-fraction'; import { getDepreciationCatalog, type DepreciationCatalog @@ -101,7 +106,12 @@ formData.description_en = snap.description_en ?? ''; formData.material_key = snap.material_key ?? ''; formData.unit_of_measure = snap.unit_of_measure ?? ''; - formData.fraction = snap.fraction ?? ''; + const mxDigits = normalizeMexTariffDigitsStored( + snap.import_tariff_code || snap.fraction || '' + ); + formData.fraction = mxDigits; + formData.import_tariff_code = mxDigits; + formData.fraction_uma_key = mxDigits.length === 10 ? mxDigits.slice(8, 10) : ''; formData.us_fraction = snap.us_fraction ?? ''; // Mapear depreciation_rate a annual_depreciation_rate si existe (números pueden ser null) formData.annual_depreciation_rate = @@ -121,7 +131,6 @@ snap.physical_review === true || snap.physical_review === '1'; formData.carta_porte_code = snap.carta_porte_code ?? ''; - formData.import_tariff_code = snap.import_tariff_code ?? ''; formData.import_tariff_type = snap.import_tariff_type ?? ''; formData.export_tariff_code = snap.export_tariff_code ?? ''; formData.export_tariff_type = snap.export_tariff_type ?? ''; @@ -191,11 +200,11 @@ // loadFractions removed function selectFraction(fraction: TariffFraction) { - formData.fraction = fraction.fraction; + const digits = buildMexTariffDigitsFromCatalogRow(fraction); + formData.fraction = digits; formData.fraction_umt = (fraction.umt ?? '') as string; - formData.fraction_uma_key = (fraction.nico ?? '') as string; - // Actualizar tarifa de importación - formData.import_tariff_code = fraction.fraction || ''; + formData.fraction_uma_key = digits.slice(8, 10); + formData.import_tariff_code = digits; formData.import_tariff_type = (fraction.umt ?? '') as string; showFractionDialog = false; } @@ -772,12 +781,13 @@
validateField('fraction')} />
@@ -3595,7 +3638,11 @@ {currentTariffDetails?.fraction || formData.fraction}{currentTariffDetails + ? formatMexTariffDigitsForDisplay( + buildMexTariffDigitsFromCatalogRow(currentTariffDetails) + ) + : formatMexTariffDigitsForDisplay(formData.fraction)} {currentTariffDetails?.umt || '-'} {currentTariffDetails?.um_code || '-'} diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/main-data.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/main-data.svelte index 648677c0..8b5b80e1 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/main-data.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/main-data.svelte @@ -9,7 +9,12 @@ import ClassDialog from './class-dialog.svelte'; import type { Item, LineQuantities, LineFinancials, LineCustoms } from '$lib/api/dashboard/a76/items'; import type { Invoice } from '$lib/api/dashboard/a76/invoices'; - import { companyStore } from '$lib/stores/company.svelte'; + import { companyStore } from '$lib/stores/company.svelte'; + import { + buildMexTariffDigitsFromCatalogRow, + formatMexTariffDigitsForDisplay, + normalizeMexTariffDigitsStored + } from '$lib/utils/mexican-tariff-fraction'; import { m } from '$lib/i18n/messages'; let { @@ -36,33 +41,13 @@ let showCountryDialog = $state(false); let showFractionDialog = $state(false); - // Format fraction with dots for display - let fractionDisplay = $derived(() => { - const frac = customs.fraction; - if (!frac) return ''; - // If already has dots, return as is - if (frac.includes('.')) return frac; - // Format 8-digit fraction as XX.XX.XX.XX - if (frac.length === 8) { - return `${frac.slice(0, 4)}.${frac.slice(4, 6)}.${frac.slice(6, 8)}`; - } - // Format 10-digit fraction as XX.XX.XX.XXXX - if (frac.length === 10) { - return `${frac.slice(0, 4)}.${frac.slice(4, 6)}.${frac.slice(6, 8)}.${frac.slice(8, 10)}`; - } - return frac; - }); + let fractionDisplay = $derived(formatMexTariffDigitsForDisplay(customs.fraction ?? '')); let isDischargeActive = $derived(lineItem.fa_data?.discharge === true); // Track previous class_id to detect changes let previousClassId = $state(undefined); - function normalizeFractionForBackend(raw: unknown) { - if (raw === null || raw === undefined) return undefined; - return String(raw).replace(/\./g, '').trim(); - } - async function getUnitByCode(unitCode: string) { const activeCompanyId = companyStore?.activeCompany?.id; if (!unitCode || !activeCompanyId) return undefined; @@ -122,7 +107,9 @@ // Fracción arancelaria (Mex / SCAII) if (!customs.fraction && classItem.fraction != null) { - customs.fraction = normalizeFractionForBackend(classItem.fraction); + customs.fraction = normalizeMexTariffDigitsStored( + String(classItem.import_tariff_code || classItem.fraction || '') + ); } // Tipo de fracción @@ -195,10 +182,7 @@ } function handleFractionSelect(fraction: any) { - // Save without dots for backend, concatenating fraction + nico (8 + 2 = 10 chars) - const fractionBase = fraction.fraction?.replace(/\./g, '') || ''; - const nico = fraction.nico || ''; - customs.fraction = fractionBase + nico; + customs.fraction = buildMexTariffDigitsFromCatalogRow(fraction); (customs as any).fraction_description = fraction.description; } @@ -456,7 +440,7 @@
{ - const fraction = editingItem.customs?.fraction; - if (!fraction) return ''; - if (fraction.includes('.')) return fraction; - if (fraction.length === 8) return `${fraction.slice(0, 4)}.${fraction.slice(4, 6)}.${fraction.slice(6, 8)}`; - if (fraction.length === 10) return `${fraction.slice(0, 4)}.${fraction.slice(4, 6)}.${fraction.slice(6, 8)}.${fraction.slice(8, 10)}`; - return fraction; - }); + const fractionDisplay = $derived( + formatMexTariffDigitsForDisplay(editingItem.customs?.fraction ?? '') + ); const systemLabel = $derived.by(() => { switch ((invoice?.system || '').toLowerCase()) { @@ -467,11 +465,22 @@
{#if editingItem?.customs} - +
+ (showFractionDialog = true)} + tabindex="0" + onkeydown={(event) => + openOnEnterOrSpace(event, () => (showFractionDialog = true))} + /> + +
{/if}
diff --git a/frontend/src/lib/utils/mexican-tariff-fraction.test.ts b/frontend/src/lib/utils/mexican-tariff-fraction.test.ts new file mode 100644 index 00000000..5cc65836 --- /dev/null +++ b/frontend/src/lib/utils/mexican-tariff-fraction.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect } from 'vitest'; +import { + buildMexTariffDigitsFromCatalogRow, + formatMexTariffDigitsForDisplay, + normalizeMexTariffDigitsStored, + splitMexTariffDigitsForSitar +} from './mexican-tariff-fraction'; + +describe('mexican-tariff-fraction', () => { + describe('buildMexTariffDigitsFromCatalogRow', () => { + it('concatena fracción punteada y NICO', () => { + expect( + buildMexTariffDigitsFromCatalogRow({ + fraction: '0101.21.01', + nico: '01', + code: '01012101' + }) + ).toBe('0101210101'); + }); + it('usa code si fraction vacía', () => { + expect( + buildMexTariffDigitsFromCatalogRow({ + fraction: '', + nico: '02', + code: '84713001' + }) + ).toBe('8471300102'); + }); + it('NICO vacío devuelve 00', () => { + expect( + buildMexTariffDigitsFromCatalogRow({ + fraction: '0101.21.01', + nico: null, + code: '01012101' + }) + ).toBe('0101210100'); + }); + }); + + describe('splitMexTariffDigitsForSitar', () => { + it('separa 10 dígitos', () => { + expect(splitMexTariffDigitsForSitar('0101210101')).toEqual({ + fraccion8: '01012101', + nico2: '01' + }); + }); + }); + + describe('normalizeMexTariffDigitsStored', () => { + it('8 dígitos añade NICO 00', () => { + expect(normalizeMexTariffDigitsStored('01012101')).toBe('0101210100'); + }); + it('respeta 10 dígitos', () => { + expect(normalizeMexTariffDigitsStored('0101210103')).toBe('0101210103'); + }); + }); + + describe('formatMexTariffDigitsForDisplay', () => { + it('formatea 8 y 10 dígitos', () => { + expect(formatMexTariffDigitsForDisplay('01012101')).toBe('0101.21.01'); + expect(formatMexTariffDigitsForDisplay('0101210101')).toBe('0101.21.01.01'); + }); + it('respeta valor con puntos', () => { + expect(formatMexTariffDigitsForDisplay('0101.21.01')).toBe('0101.21.01'); + }); + }); +}); diff --git a/frontend/src/lib/utils/mexican-tariff-fraction.ts b/frontend/src/lib/utils/mexican-tariff-fraction.ts new file mode 100644 index 00000000..2973a179 --- /dev/null +++ b/frontend/src/lib/utils/mexican-tariff-fraction.ts @@ -0,0 +1,64 @@ +/** + * Fracción arancelaria mexicana: 8 dígitos de fracción + 2 de NICO (10 sin puntos). + */ + +export type MexTariffCatalogRow = { + fraction?: string | null; + nico?: string | null; + code?: string | null; +}; + +function digitsOnly(s: string): string { + return s.replace(/\D/g, ''); +} + +/** 10 dígitos: base (8) + NICO (2), sin puntos. */ +export function buildMexTariffDigitsFromCatalogRow(tf: MexTariffCatalogRow): string { + const fromFraction = String(tf.fraction ?? '') + .replace(/\./g, '') + .replace(/\D/g, ''); + const fromCode = digitsOnly(String(tf.code ?? '')); + let base8 = fromFraction.slice(0, 8); + if (base8.length === 0) base8 = fromCode.slice(0, 8); + if (base8.length < 8) base8 = base8.padEnd(8, '0'); + + const nico2 = digitsOnly(String(tf.nico ?? '')).slice(-2).padStart(2, '0'); + return `${base8}${nico2}`.slice(0, 10); +} + +export function splitMexTariffDigitsForSitar(digitsOrLegacy: string | null | undefined): { + fraccion8: string; + nico2: string; +} { + const raw = String(digitsOrLegacy ?? '').replace(/\./g, '').replace(/\D/g, ''); + const fraccion8 = raw.slice(0, 8).padEnd(8, '0'); + const nico2 = (raw.length >= 10 ? raw.slice(8, 10) : '00').padStart(2, '0'); + return { fraccion8, nico2: nico2.slice(0, 2) }; +} + +/** + * Formato de vista: 8 → XXXX.XX.XX, 10 → XXXX.XX.XX.NN. + * Si ya contiene puntos, se devuelve igual (datos legados). + */ +export function formatMexTariffDigitsForDisplay(value: string | null | undefined): string { + const frac = String(value ?? '').trim(); + if (!frac) return ''; + if (frac.includes('.')) return frac; + const digits = frac.replace(/\D/g, ''); + if (digits.length === 8) { + return `${digits.slice(0, 4)}.${digits.slice(4, 6)}.${digits.slice(6, 8)}`; + } + if (digits.length === 10) { + return `${digits.slice(0, 4)}.${digits.slice(4, 6)}.${digits.slice(6, 8)}.${digits.slice(8, 10)}`; + } + return frac; +} + +/** Migra valor legado (puntos o 8 dígitos) a 10 dígitos sin puntos. */ +export function normalizeMexTariffDigitsStored(raw: string | null | undefined): string { + const d = String(raw ?? '').replace(/\D/g, ''); + if (!d) return ''; + if (d.length >= 10) return d.slice(0, 10); + if (d.length === 8) return `${d}00`; + return d.padEnd(10, '0').slice(0, 10); +} diff --git a/frontend/src/routes/dashboard/goods/fixed-asset-classes/+page.svelte b/frontend/src/routes/dashboard/goods/fixed-asset-classes/+page.svelte index 215ea02f..ccc192e5 100644 --- a/frontend/src/routes/dashboard/goods/fixed-asset-classes/+page.svelte +++ b/frontend/src/routes/dashboard/goods/fixed-asset-classes/+page.svelte @@ -12,6 +12,10 @@ import { onMount } from 'svelte'; import DataTable from '$lib/components/dashboard/goods/classes/data-table.svelte'; import { columns } from '$lib/components/dashboard/goods/classes/columns'; + import { + formatMexTariffDigitsForDisplay, + normalizeMexTariffDigitsStored + } from '$lib/utils/mexican-tariff-fraction'; // Tipo extendido que combina A76Class y FAClass interface FixedAssetClassExtended extends A76Class { @@ -151,7 +155,9 @@ description_en: cls.description_en || '', material_key: cls.material_key || '', unit_of_measure: cls.unit_of_measure || '', - fraction: cls.fraction || '', + fraction: normalizeMexTariffDigitsStored( + (cls as FixedAssetClassExtended).import_tariff_code || cls.fraction || '' + ), us_fraction: cls.us_fraction || '', unit_measure_trade: '', depreciation_rate: (cls as FixedAssetClassExtended).depreciation_rate || null, @@ -399,7 +405,7 @@ >Fracción Arancelaria

- {formData.fraction || '0000.00.00'} + {formatMexTariffDigitsForDisplay(formData.fraction) || '—'}

@@ -512,6 +518,9 @@ try { const cleanData = $state.snapshot(data); + const mxFraccionNorm = normalizeMexTariffDigitsStored( + String(cleanData.import_tariff_code || cleanData.fraction || '').trim() + ); const companyId = companyStore.activeCompany?.id; if (!companyId) { @@ -531,7 +540,7 @@ description_en: cleanData.description_en?.trim() || '', material_key: cleanData.material_key?.trim() || '', unit_of_measure: cleanData.unit_of_measure?.trim() || '', - fraction: cleanData.fraction?.trim() || '', + fraction: mxFraccionNorm, us_fraction: cleanData.us_fraction || '', physical_review: cleanData.physical_review ? 1 : 0, iva_exempt_fraction: cleanData.iva_exempt_fraction || '' @@ -554,7 +563,7 @@ cleanData.depreciation_rate != null ? Number(cleanData.depreciation_rate) : null, - import_tariff_code: cleanData.import_tariff_code || null, + import_tariff_code: mxFraccionNorm || null, import_tariff_type: cleanData.import_tariff_type || null, export_tariff_code: cleanData.export_tariff_code || null, export_tariff_type: cleanData.export_tariff_type || null, @@ -598,7 +607,7 @@ description_en: cleanData.description_en?.trim() || '', material_key: cleanData.material_key?.trim() || '', unit_of_measure: cleanData.unit_of_measure?.trim() || '', - fraction: cleanData.fraction?.trim() || '', + fraction: mxFraccionNorm, us_fraction: cleanData.us_fraction?.trim() || '', sub_key: cleanData.sub_key || '', physical_review: cleanData.physical_review ? 1 : 0, diff --git a/frontend/src/routes/dashboard/goods/parts/+page.svelte b/frontend/src/routes/dashboard/goods/parts/+page.svelte index ec709e86..7ec64448 100644 --- a/frontend/src/routes/dashboard/goods/parts/+page.svelte +++ b/frontend/src/routes/dashboard/goods/parts/+page.svelte @@ -12,6 +12,7 @@ import DataTable from '$lib/components/dashboard/goods/parts/data-table.svelte'; import { createColumns } from '$lib/components/dashboard/goods/parts/columns'; import { obtenerAtajosListaMercancias } from '$lib/config/shortcuts/dashboard/goods/list'; + import { formatMexTariffDigitsForDisplay } from '$lib/utils/mexican-tariff-fraction'; // Estado de la lista de partes let parts = $state([]); @@ -394,7 +395,7 @@ >Fracción Arancelaria

- {selectedPart.fraction || '0000.00.00'} + {formatMexTariffDigitsForDisplay(selectedPart.fraction || '') || '—'}