From 39fdfca9179fbc2af31a55319ebb1d4db692f6fb Mon Sep 17 00:00:00 2001 From: hreyes Date: Fri, 17 Apr 2026 10:22:08 -0600 Subject: [PATCH] fix/partida-campos-obligatorios --- .../a76/items/exports/validators/common.py | 51 +++++++++++++++++-- .../us-fraction-selector-dialog.svelte | 41 +++++++++++---- .../edit/items/fa/item-sheet-fa.svelte | 6 ++- .../invoices/edit/items/fa/main-data.svelte | 5 +- .../edit/items/fa/packages-section.svelte | 40 +++++++++++++-- frontend/src/lib/utils/items-logic.ts | 47 ++++++++++++----- 6 files changed, 159 insertions(+), 31 deletions(-) diff --git a/backend/api/v1/modules/a76/items/exports/validators/common.py b/backend/api/v1/modules/a76/items/exports/validators/common.py index c8622b97..d8e204f5 100644 --- a/backend/api/v1/modules/a76/items/exports/validators/common.py +++ b/backend/api/v1/modules/a76/items/exports/validators/common.py @@ -14,6 +14,9 @@ from api.v1.modules.a76.invoices.models import InvoiceHeader from api.v1.modules.a76.classes.models import Class from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure from api.v1.modules.a76.general_catalogs.packages.models import Package +from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import ( + USTariffFraction, +) from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem from api.v1.modules.public.reference_data.countries.models import Country from api.v1.modules.a76.general_catalogs.sectors.models import Sector @@ -360,18 +363,56 @@ def validate_common( ) if line.customs.american_fraction: - american_fraction_exists = db.query( - exists().where( - LineCustom.american_fraction == line.customs.american_fraction + def _normalize_american_fraction_code(raw_code: str) -> list[str]: + normalized_raw = (raw_code or "").strip() + if not normalized_raw: + return [] + + digits_only = normalized_raw.replace(".", "").replace(" ", "").replace("-", "") + candidates = [normalized_raw] + + if len(digits_only) == 10: + candidates.append( + f"{digits_only[:4]}.{digits_only[4:6]}.{digits_only[6:8]}.{digits_only[8:10]}" + ) + elif len(digits_only) == 8: + candidates.append(f"{digits_only[:4]}.{digits_only[4:6]}.{digits_only[6:8]}") + + candidates.append(digits_only) + + seen: set[str] = set() + deduped: list[str] = [] + for candidate in candidates: + if not candidate or candidate in seen: + continue + seen.add(candidate) + deduped.append(candidate) + return deduped + + candidates = _normalize_american_fraction_code(line.customs.american_fraction) + us_fraction: USTariffFraction | None = None + for candidate in candidates: + us_fraction = ( + db.query(USTariffFraction) + .filter( + USTariffFraction.code == candidate, + USTariffFraction.tenant_id == tenant_id, + USTariffFraction.company_id == company_id, + ) + .first() ) - ).scalar() - if not american_fraction_exists: + if us_fraction: + break + + if not us_fraction: errors.add_error( field=f"line[{line_number}].customs.american_fraction", message="La fracción americana especificada no existe.", solution=["Proporciona una fracción americana valida."], code="AMERICAN_FRACTION_NOT_FOUND", ) + else: + line.customs.american_fraction = us_fraction.code if line.order: if len(line.order) > 20: diff --git a/frontend/src/lib/components/dashboard/goods/modales/us-fraction-selector-dialog.svelte b/frontend/src/lib/components/dashboard/goods/modales/us-fraction-selector-dialog.svelte index 4ba7a2a6..b581144c 100644 --- a/frontend/src/lib/components/dashboard/goods/modales/us-fraction-selector-dialog.svelte +++ b/frontend/src/lib/components/dashboard/goods/modales/us-fraction-selector-dialog.svelte @@ -21,32 +21,55 @@ let items = $state([]); let loading = $state(false); let searchTerm = $state(""); - let loaded = $state(false); + let loadedForCompanyId = $state(null); + + const activeCompanyId = $derived(companyStore.activeCompany?.id); + + function normalizeAmericanFractionCode(code: string) { + return (code || '').replace(/[.\s-]/g, ''); + } + + function isEligibleAmericanFraction(item: USTariffFraction) { + const normalizedCode = normalizeAmericanFractionCode(item.code || ''); + return /^\d{8}$/.test(normalizedCode) || /^\d{10}$/.test(normalizedCode); + } // Filtro local let filteredItems = $derived( items.filter(i => - (i.code || "").includes(searchTerm) || + isEligibleAmericanFraction(i) && + ((i.code || "").includes(searchTerm) || (i.description || "").toLowerCase().includes(searchTerm.toLowerCase()) + ) ) ); // Cargar datos al abrir $effect(() => { - if (open && !loaded && companyStore.activeCompany?.id) { - loadFractions(); + if (!open) return; + + if (!activeCompanyId) { + items = []; + loadedForCompanyId = null; + return; + } + + if (loadedForCompanyId !== activeCompanyId) { + searchTerm = ''; + items = []; + void loadFractions(activeCompanyId); } }); - async function loadFractions() { - if (!companyStore.activeCompany?.id) { + async function loadFractions(companyId: number) { + if (!companyId) { toast.error("No hay empresa seleccionada"); return; } loading = true; try { - const response = await getUSTariffFractions(1, 1000, companyStore.activeCompany.id); + const response = await getUSTariffFractions(1, 1000, companyId); if (response.error) { console.error("Error al cargar fracciones americanas:", response.error); @@ -55,8 +78,8 @@ } if (response.data?.items) { - items = response.data.items; - loaded = true; + items = response.data.items.filter((item) => isEligibleAmericanFraction(item)); + loadedForCompanyId = companyId; } else { console.warn("No se encontraron fracciones americanas:", response); toast.info("No se encontraron fracciones americanas registradas"); diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-sheet-fa.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-sheet-fa.svelte index ef166084..37573833 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-sheet-fa.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-sheet-fa.svelte @@ -75,6 +75,9 @@ if (editingItem.fa_data.discharge === undefined) { editingItem.fa_data.discharge = false; } + if ((editingItem.fa_data.movement_type_import === undefined || editingItem.fa_data.movement_type_import === '') && (showLinkToImportBlock || showRepairBlock)) { + editingItem.fa_data.movement_type_import = 'TEM'; + } } }); @@ -357,6 +360,7 @@
+

Los campos marcados con * son obligatorios.

{ @@ -483,7 +487,7 @@
- + Main Data +

+ Los campos marcados con * son obligatorios. +

@@ -448,7 +451,7 @@
- +
(0); let isLoadingPackage = $state(false); @@ -114,10 +116,21 @@ package_weight_unit = pkg.weight_unit || 0; quantities.package_description = pkg.description_es || pkg.description_en || pkg.key; } + + function handleAmericanFractionSelect(fraction: any) { + customs.american_fraction = fraction.code || ''; + (customs as any).american_fraction_description = fraction.description || ''; + if (fraction.ad_valorem !== null && fraction.ad_valorem !== undefined) { + customs.advalorem_american = fraction.ad_valorem; + } + }
PACKAGES +

+ Los campos marcados con * son obligatorios. +

@@ -169,7 +182,7 @@
WEIGHTS
- +
@@ -200,8 +213,28 @@
- - + +
+ !disabled && (americanFractionDialogOpen = true)} + /> + +
@@ -230,3 +263,4 @@
+ diff --git a/frontend/src/lib/utils/items-logic.ts b/frontend/src/lib/utils/items-logic.ts index c5a227bf..f9f0b4e5 100644 --- a/frontend/src/lib/utils/items-logic.ts +++ b/frontend/src/lib/utils/items-logic.ts @@ -259,12 +259,42 @@ const FIELD_MAP: Record = { 'financial.unit_cost_capture': 'Costo Unitario', 'customs.fraction': 'Fracción Arancelaria', 'customs.origin_country': 'País de Origen', + 'customs.american_fraction': 'Fracción Americana', 'fa_data.search_invoice': 'Factura de Referencia', 'fa_data.search_line': 'Línea de Referencia', 'fa_data.search_type': 'Tipo de Búsqueda', 'fa_data.movement_type_import': 'Tipo de Importación' }; +function humanizeFieldPath(field: string): string { + const rawField = (field || '').trim(); + if (!rawField) return 'campo'; + + const lineMatch = rawField.match(/^line\[(\d+)\]\.(.+)$/i); + const fieldPath = lineMatch?.[2] || rawField; + const mappedPath = fieldPath.replace(/^body\./i, ''); + const fieldLabel = FIELD_MAP[mappedPath] || mappedPath.replace(/\./g, ' → '); + + if (lineMatch) { + return `Partida ${lineMatch[1]} - ${fieldLabel}`; + } + + return fieldLabel; +} + +function humanizeValidationMessage(message: string): string { + const rawMessage = (message || '').trim(); + if (!rawMessage) return 'error de validación'; + + return rawMessage + .replace(/line\[(\d+)\]\.(\w+(?:\.\w+)*)/gi, (_match, lineNumber, fieldPath) => { + return `Partida ${lineNumber} - ${humanizeFieldPath(fieldPath)}`; + }) + .replace(/\b(field required|is required)\b/gi, 'es obligatorio') + .replace(/\b(value is not a valid decimal)\b/gi, 'debe ser un número válido') + .replace(/\b(value is not a valid integer)\b/gi, 'debe ser un número entero válido'); +} + /** * Formats a backend error into a human-readable Spanish message. * Handles 422 (Validation), 403 (Forbidden), 404 (Not Found), and 500 (Server Error). @@ -285,12 +315,8 @@ export function formatItemError(error: any): string { // New structure (ApiResponse.validationErrors) if (status === 422 && Array.isArray(validationErrors)) { const errors = validationErrors.map((err: any) => { - const field = err.field || ''; - const fieldName = FIELD_MAP[field] || field || 'campo'; - - let msg = err.message || 'error de validación'; - if (msg.includes('field required')) msg = 'es obligatorio'; - if (msg.includes('value is not a valid decimal')) msg = 'debe ser un número válido'; + const fieldName = humanizeFieldPath(err.field || ''); + const msg = humanizeValidationMessage(err.message || 'error de validación'); return `• ${fieldName}: ${msg}`; }); @@ -305,11 +331,8 @@ export function formatItemError(error: any): string { .filter((l: string) => l !== 'body') .join('.'); - const fieldName = FIELD_MAP[locPath] || locPath || 'campo'; - - let msg = err.msg || 'error de validación'; - if (msg.includes('field required')) msg = 'es obligatorio'; - if (msg.includes('value is not a valid decimal')) msg = 'debe ser un número válido'; + const fieldName = humanizeFieldPath(locPath); + const msg = humanizeValidationMessage(err.msg || 'error de validación'); return `• ${fieldName}: ${msg}`; }); @@ -322,7 +345,7 @@ export function formatItemError(error: any): string { if (d.includes('Access denied')) return 'No tienes permisos para realizar esta acción.'; if (d.includes('not found')) return 'El registro no existe o fue eliminado.'; if (d.includes('Class mismatch')) return 'Error de validación: ' + d; - return d; + return humanizeValidationMessage(d); } // 4. Fallbacks by status code