From 97bc4075317ef4ac16e6fc6b18175a43bd94abaa Mon Sep 17 00:00:00 2001 From: hreyes Date: Wed, 18 Mar 2026 14:44:45 -0600 Subject: [PATCH] feature/partidas-impo-bug-fix --- .../a76/items/imports/validators/common.py | 72 +++++++++++- .../invoices/edit/items/fa/main-data.svelte | 109 +++++++++++++----- .../edit/items/fa/packages-section.svelte | 27 ++++- 3 files changed, 171 insertions(+), 37 deletions(-) diff --git a/backend/api/v1/modules/a76/items/imports/validators/common.py b/backend/api/v1/modules/a76/items/imports/validators/common.py index b1d0dc2f..ad62f477 100644 --- a/backend/api/v1/modules/a76/items/imports/validators/common.py +++ b/backend/api/v1/modules/a76/items/imports/validators/common.py @@ -9,6 +9,9 @@ from ...common.fractions import search_fraction_preference from ...common.common_validators import item_exists from ...models import LineItem from ...line_customs.models import FractionType, LineCustom +from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import ( + USTariffFraction, +) from api.v1.modules.a76.items.schemas import LineItemCreate from api.v1.modules.a76.invoices.models import InvoiceHeader from api.v1.modules.a76.classes.models import Class @@ -22,6 +25,8 @@ from api.v1.modules.public.reference_data.valuation_methods.models import ( from api.v1.modules.a76.parts.models import Part from api.v1.modules.a76.general_catalogs.company.models import Company +import re + def validate_common( db: Session, @@ -284,18 +289,75 @@ 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]: + """ + Attempts to map user input to the canonical USTariffFraction.code. + + The catalog commonly stores dotted HTS codes (e.g. 3802.20.00.00), + but users may paste/enter digits-only or use different separators. + """ + + normalized_raw = (raw_code or "").strip() + if not normalized_raw: + return [] + + digits_only = re.sub(r"[.\s\-]", "", normalized_raw) + + candidates: list[str] = [] + + # 1) Exact input + candidates.append(normalized_raw) + + # 2) Canonical with dots if length matches common patterns + 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]}" + ) + + # 3) Digits-only (if catalog stores without dots) + candidates.append(digits_only) + + # De-duplicate while preserving order + seen: set[str] = set() + deduped: list[str] = [] + for c in candidates: + if not c or c in seen: + continue + seen.add(c) + deduped.append(c) + return deduped + + raw_american_fraction = str(line.customs.american_fraction) + candidates = _normalize_american_fraction_code(raw_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: + # Keep canonical value so downstream validators can use it safely. + line.customs.american_fraction = us_fraction.code if line.order: if len(line.order) > 20: 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 8011fde5..f279e7cb 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 @@ -50,6 +50,84 @@ // 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; + + try { + const res = await fetch( + `/api-sveltekit/units-of-measure?code=${encodeURIComponent(unitCode)}&page=1&page_size=20`, + { + method: 'GET', + credentials: 'include' + } + ); + if (!res.ok) return undefined; + const data = await res.json(); + const units = data?.items || data?.data || data; + if (Array.isArray(units) && units.length > 0) return units[0]; + } catch { + // Ignore unit lookup failures and let the user fix manually. + } + + return undefined; + } + + async function applyClassDefaults(classItem: any) { + if (!classItem) return; + + // Class display fields + (lineItem as any).class_code = classItem.class_code; + (lineItem as any).class_unit_of_measure = classItem.unit_of_measure; + (lineItem as any).class_description = classItem.description_es || classItem.description_en; + + // Descriptions + if ((lineItem as any).description) { + const desc = (lineItem as any).description; + // Use != null (instead of truthy) to support valid empty strings + if (classItem.description_es != null) desc.description_spanish = classItem.description_es; + if (classItem.description_en != null) desc.description_english = classItem.description_en; + } + + // U.M. (display + internal FK id) + const classUnitCode = classItem.unit_of_measure; + if (classUnitCode != null) { + // Always set display code if missing. + if (!(lineItem as any).unit_code) (lineItem as any).unit_code = classUnitCode; + + // Only resolve and override the FK if it isn't set yet. + if (!lineItem.unit_of_measure) { + const unit = await getUnitByCode(String(classUnitCode)); + if (unit) { + lineItem.unit_of_measure = unit.id; + (lineItem as any).unit_code = unit.code; + (lineItem as any).unit_description = unit.description || unit.description_en; + quantities.unit_of_measure = unit.code; + } + } + } + + // Fracción arancelaria (Mex / SCAII) + if (!customs.fraction && classItem.fraction != null) { + customs.fraction = normalizeFractionForBackend(classItem.fraction); + } + + // Tipo de fracción + if (!customs.fraction_type && classItem.import_tariff_type) { + customs.fraction_type = classItem.import_tariff_type; + } + + // Fracción americana (HTS / US) + if (!customs.american_fraction && classItem.us_fraction) { + customs.american_fraction = classItem.us_fraction; + } + } + // Watch for class_id changes and update descriptions automatically $effect(() => { const currentClassId = lineItem.class_id; @@ -72,20 +150,8 @@ const classItem = classes.find((c: any) => c.id === currentClassId); if (classItem) { - // Store the code and description in the lineItem for display - (lineItem as any).class_code = classItem.class_code; - (lineItem as any).class_unit_of_measure = classItem.unit_of_measure; - (lineItem as any).class_description = classItem.description_es || classItem.description_en; - - // Update description fields if description object exists - if ((lineItem as any).description) { - if (classItem.description_es) { - (lineItem as any).description.description_spanish = classItem.description_es; - } - if (classItem.description_en) { - (lineItem as any).description.description_english = classItem.description_en; - } - } + // Apply defaults derived from catalog class selection. + void applyClassDefaults(classItem); } }) .catch(error => { @@ -96,20 +162,7 @@ function handleClassSelect(classItem: any) { lineItem.class_id = classItem.id; - // Store the unit of measure and description for display - (lineItem as any).class_unit_of_measure = classItem.unit_of_measure; - (lineItem as any).class_code = classItem.class_code; - (lineItem as any).class_description = classItem.description_es || classItem.description_en; - - // Update description fields if description object exists - if ((lineItem as any).description) { - if (classItem.description_es) { - (lineItem as any).description.description_spanish = classItem.description_es; - } - if (classItem.description_en) { - (lineItem as any).description.description_english = classItem.description_en; - } - } + void applyClassDefaults(classItem); }; function handleUnitSelect(unit: any) { diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte index d0ba2383..61c68f09 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte @@ -37,10 +37,29 @@ }); // Calculate total package weight - const totalPackageWeight = $derived.by(() => { - const qty = quantities.package_quantity || 0; - const weightPerUnit = package_weight_unit || 0; - return (qty * weightPerUnit).toFixed(4); + const totalPackageWeightNum = $derived.by(() => { + const qty = Number(quantities.package_quantity ?? 0); + const weightPerUnit = Number(package_weight_unit ?? 0); + return qty * weightPerUnit; + }); + + const totalPackageWeight = $derived.by(() => totalPackageWeightNum.toFixed(4)); + const computedGrossWeight = $derived.by(() => { + const net = Number(quantities.net_weight ?? 0); + return net + totalPackageWeightNum; + }); + + // Auto-calculate gross weight = net weight + weight of packages (if gross wasn't provided) + $effect(() => { + const netWeight = quantities.net_weight; + if (netWeight === null || netWeight === undefined) return; + + const currentGross = quantities.gross_weight; + const shouldAutoFill = currentGross === null || currentGross === undefined || Number(currentGross) === 0; + if (!shouldAutoFill) return; + + // Keep precision stable enough for the UI inputs. + quantities.gross_weight = Number(computedGrossWeight.toFixed(8)); }); // Load or sync package data when package_id exists