From 960fca190f567d9ae03d064496111d9c5774d2d2 Mon Sep 17 00:00:00 2001 From: acazares Date: Mon, 16 Feb 2026 09:49:07 -0600 Subject: [PATCH 1/3] feat: add fractions section to sidebar with translations in English and Spanish --- frontend/messages/en.json | 10 ++++++ frontend/messages/es.json | 10 ++++++ .../src/lib/components/sidebar/modules.ts | 36 +++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/frontend/messages/en.json b/frontend/messages/en.json index cf838b4f..fc56fcdf 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -63,6 +63,16 @@ "back_flush": "Back Flush", "crossing_notice": "Crossing Notice" }, + "fractions": { + "title": "Fractions", + "sitar": "Fraction Sitar", + "sitar_seventh_amendment": "Fraction Sitar - Seventh Amendment", + "sitar_us": "Fraction Sitar US", + "american": "Fraction American", + "canadian": "Fraction Canadian", + "historical": "Fraction Historical", + "sectors": "Sectors" + }, "goods": { "title": "Goods", "classes": "Classes", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 1ea855b2..413726cd 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -62,6 +62,16 @@ "electronic_notices": "Avisos electrónicos", "back_flush": "Back Flush", "crossing_notice": "Aviso de cruce" + }, + "fractions": { + "title": "Fracciones", + "sitar": "Fracciones Sitar", + "sitar_seventh_amendment": "Fracciones Sitar - 7ma enmienda", + "sitar_us": "Fracciones Sitar US", + "american": "Fracciones Americana", + "canadian": "Fracciones Canadiense", + "historical": "Fracciones Historicas", + "sectors": "Sectores" }, "goods": { "title": "Mercancías", diff --git a/frontend/src/lib/components/sidebar/modules.ts b/frontend/src/lib/components/sidebar/modules.ts index ee0a8d9a..264c7683 100644 --- a/frontend/src/lib/components/sidebar/modules.ts +++ b/frontend/src/lib/components/sidebar/modules.ts @@ -8,6 +8,7 @@ import { FileText, Frame, GalleryVerticalEnd, + Hash, LayoutDashboard, Package, Settings2, @@ -296,6 +297,41 @@ export function getSidebarData(): SidebarData { }, ], }, + { + title: m["sidebar.fractions.title"](), + url: "#", + icon: Hash, + items: [ + { + title: m["sidebar.fractions.sitar"](), + url: "#", + }, + { + title: m["sidebar.fractions.sitar_seventh_amendment"](), + url: "#", + }, + { + title: m["sidebar.fractions.sitar_us"](), + url: "#", + }, + { + title: m["sidebar.fractions.american"](), + url: "#", + }, + { + title: m["sidebar.fractions.canadian"](), + url: "#", + }, + { + title: m["sidebar.fractions.historical"](), + url: "#", + }, + { + title: m["sidebar.fractions.sectors"](), + url: "#", + }, + ], + }, { title: m["sidebar.goods.title"](), url: "#", From 5ca98ad0bc1e423dbc85066afc5d72c9bb3018d1 Mon Sep 17 00:00:00 2001 From: acazares Date: Mon, 16 Feb 2026 09:49:23 -0600 Subject: [PATCH 2/3] fix: correct logic for handling remesa and package quantity validation in invoice updates --- .../modules/a76/invoices/imports/temporary/validators/update.py | 2 +- .../v1/modules/a76/items/imports/temporary/validators/common.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/api/v1/modules/a76/invoices/imports/temporary/validators/update.py b/backend/api/v1/modules/a76/invoices/imports/temporary/validators/update.py index d30069a4..9c254a9b 100644 --- a/backend/api/v1/modules/a76/invoices/imports/temporary/validators/update.py +++ b/backend/api/v1/modules/a76/invoices/imports/temporary/validators/update.py @@ -64,7 +64,7 @@ def validate_update( # Columna B: Remesa if invoice_data.compliance_mx.remesa: - invoice_data.compliance_mx.remesa = clean_str(invoice_data.compliance_mx.remesa) + invoice_data.compliance_mx.remesa = invoice_data.compliance_mx.remesa else: invoice_data.compliance_mx.remesa = existing_invoice.compliance_mx.remesa if existing_invoice.compliance_mx else None diff --git a/backend/api/v1/modules/a76/items/imports/temporary/validators/common.py b/backend/api/v1/modules/a76/items/imports/temporary/validators/common.py index d10c95fd..3cbe5958 100644 --- a/backend/api/v1/modules/a76/items/imports/temporary/validators/common.py +++ b/backend/api/v1/modules/a76/items/imports/temporary/validators/common.py @@ -164,7 +164,7 @@ def validate_common( code="PACKAGE_QUANTITY_MUST_BE_GREATER_THAN_ZERO", ) else: - if line.quantity.package_quantity and line.quantity.package_quantity > 0: + if (line.quantity.package_quantity or line.quantity.package_quantity > 0) and not line.quantity.package_id: errors.add_error( field=f"line[{line_number}].quantity.package_id", message="El paquete es obligatorio cuando se proporciona la cantidad de paquetes.", From 9e37340dd5ea1db1f5b1d36630c0d86dcb60ceeb Mon Sep 17 00:00:00 2001 From: acazares Date: Mon, 16 Feb 2026 11:52:34 -0600 Subject: [PATCH 3/3] feat: implement package and payment method dialogs with API integration --- .../imports/temporary/validators/create.py | 10 +- .../imports/temporary/validators/update.py | 8 +- frontend/src/lib/api/dashboard/a76/items.ts | 4 +- .../edit/items/fa/item-configuration.svelte | 5 +- .../edit/items/fa/item-sheet-fa.svelte | 3 +- .../invoices/edit/items/fa/main-data.svelte | 9 - .../edit/items/fa/package-dialog.svelte | 163 ++++++++++++++++++ .../edit/items/fa/packages-section.svelte | 103 ++++++++++- .../items/fa/payment-method-dialog.svelte | 151 ++++++++++++++++ .../edit/items/fa/tab-continuation.svelte | 58 ++++++- .../invoices/edit/items/items-tab-form.svelte | 49 +++++- .../routes/api-sveltekit/packages/+server.ts | 80 +++++++++ .../api-sveltekit/payment-methods/+server.ts | 64 +++++++ 13 files changed, 674 insertions(+), 33 deletions(-) create mode 100644 frontend/src/lib/components/dashboard/invoices/edit/items/fa/package-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/invoices/edit/items/fa/payment-method-dialog.svelte create mode 100644 frontend/src/routes/api-sveltekit/packages/+server.ts create mode 100644 frontend/src/routes/api-sveltekit/payment-methods/+server.ts diff --git a/backend/api/v1/modules/a76/items/imports/temporary/validators/create.py b/backend/api/v1/modules/a76/items/imports/temporary/validators/create.py index 146a4278..66024a5e 100644 --- a/backend/api/v1/modules/a76/items/imports/temporary/validators/create.py +++ b/backend/api/v1/modules/a76/items/imports/temporary/validators/create.py @@ -211,18 +211,18 @@ def validate_create( # Calcular peso neto en kilogramos (estándar interno) if unit_is_kgs: - if invoice_weight_type == "kgs": + if invoice_weight_type == "KGS": line.quantity.net_weight = quantity else: # invoice en libras line.quantity.net_weight = quantity * Decimal("2.204624") elif unit_is_lbs: - if invoice_weight_type == "kgs": + if invoice_weight_type == "KGS": line.quantity.net_weight = quantity / Decimal("2.204624") else: # invoice en libras line.quantity.net_weight = quantity else: # Otra unidad de medida - usar peso capturado y convertir si es necesario - if invoice_weight_type == "kgs": + if invoice_weight_type == "KGS": # El peso capturado está en kilos line.quantity.net_weight = net_weight_input else: @@ -252,7 +252,7 @@ def validate_create( # Si no se proporcionó peso bruto, calcularlo if not gross_weight_input or gross_weight_input == 0: - if invoice_weight_type == "kgs": + if invoice_weight_type == "KGS": line.quantity.gross_weight = line.quantity.net_weight + ( package_weight_unit * package_quantity ) @@ -262,7 +262,7 @@ def validate_create( ) else: # Convertir peso bruto capturado según tipo de factura - if invoice_weight_type == "kgs": + if invoice_weight_type == "KGS": line.quantity.gross_weight = gross_weight_input else: # libras line.quantity.gross_weight = gross_weight_input / Decimal("2.204624") diff --git a/backend/api/v1/modules/a76/items/imports/temporary/validators/update.py b/backend/api/v1/modules/a76/items/imports/temporary/validators/update.py index 338aac13..eec6f443 100644 --- a/backend/api/v1/modules/a76/items/imports/temporary/validators/update.py +++ b/backend/api/v1/modules/a76/items/imports/temporary/validators/update.py @@ -73,7 +73,7 @@ def validate_update( # Se proporcionó nuevo peso neto, convertir según tipo net_weight_input = line.quantity.net_weight - if invoice_weight_type == "kgs": + if invoice_weight_type == "KGS": line.quantity.net_weight = net_weight_input else: # libras, convertir a kilos line.quantity.net_weight = net_weight_input / Decimal("2.204624") @@ -85,7 +85,7 @@ def validate_update( if line.quantity.gross_weight is not None: gross_weight_input = line.quantity.gross_weight - if invoice_weight_type == "kgs": + if invoice_weight_type == "KGS": line.quantity.gross_weight = gross_weight_input else: # libras, convertir a kilos line.quantity.gross_weight = gross_weight_input / Decimal("2.204624") @@ -98,8 +98,8 @@ def validate_update( line.quantity.package_quantity = existing_line.quantity.package_quantity # Clave de bultos - if not line.quantity.package_key: - line.quantity.package_key = existing_line.quantity.package_key + if not line.quantity.package_id: + line.quantity.package_id = existing_line.quantity.package_id # País de origen if not line.customs.origin_country: diff --git a/frontend/src/lib/api/dashboard/a76/items.ts b/frontend/src/lib/api/dashboard/a76/items.ts index 353ddcfa..1ec5e69d 100644 --- a/frontend/src/lib/api/dashboard/a76/items.ts +++ b/frontend/src/lib/api/dashboard/a76/items.ts @@ -53,9 +53,9 @@ export interface LineQuantities { gross_weight?: number; // Packaging - package_key?: string; + package_id?: number; // Changed from string to number - this is the ID from packages catalog package_quantity?: number; - package_description?: string; + package_description?: string; // This is for display purposes only } export interface LineDescriptions { diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-configuration.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-configuration.svelte index e71c6096..f399c4f5 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-configuration.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-configuration.svelte @@ -36,7 +36,7 @@ } function handlePartSelect(part: any) { - lineItem.part_number_id = part.id; + lineItem.part_number = part.id; // Store part number for display (lineItem as any).part_number = part.part_number; (lineItem as any).part_description_es = part.description_spanish; @@ -110,9 +110,6 @@ {#if (lineItem as any).part_description_es}

{(lineItem as any).part_description_es}

{/if} - {#if lineItem.part_number_id} -

ID: {lineItem.part_number_id}

- {/if}
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 ea64e1f0..be1ec0f3 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 @@ -65,7 +65,7 @@
-
+
{#if line} @@ -129,6 +129,7 @@ bind:descriptions={editingItem.lines![0].description!} bind:customs={editingItem.lines![0].customs!} bind:quantities={editingItem.lines![0].quantity!} + invoice={invoice} /> {(lineItem as any).class_description}

{/if} - {#if lineItem.class_id} -

ID: {lineItem.class_id}

- {/if}
@@ -183,9 +180,6 @@
- {#if (lineItem as any).unit_description} -

{(lineItem as any).unit_description}

- {/if}
@@ -217,9 +211,6 @@ - {#if (customs as any).fraction_description} -

{(customs as any).fraction_description}

- {/if} diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/package-dialog.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/package-dialog.svelte new file mode 100644 index 00000000..85e20201 --- /dev/null +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/package-dialog.svelte @@ -0,0 +1,163 @@ + + + + + + CATALOGO DE BULTOS + + +
+
+ + +
+
+ +
+ {#if loading} +
+ +
+ {:else if error} +
+

{error}

+
+ {:else} +
+ + + + + + + + + + + + {#each filteredPackages as pkg, i} + handleSelect(pkg)} + > + + + + + + + {/each} + {#if filteredPackages.length === 0} + + + + {/if} + +
ClaveDescripción EspañolDescripción InglésPeso UnitarioCódigo ACE
{pkg.key || ''}{pkg.description_es || ''}{pkg.description_en || ''}{pkg.weight_unit || ''}{pkg.code_ace || ''}
+ No se encontraron resultados +
+
+ {/if} +
+ +
+ +
+
+
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 d3c6f785..d0ba2383 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 @@ -1,21 +1,95 @@
@@ -28,7 +102,24 @@
- +
+ + +
@@ -36,10 +127,10 @@
- +
- +
@@ -57,7 +148,7 @@
- KILOS + {weightUnitLabel}
@@ -99,3 +190,5 @@
+ + diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/payment-method-dialog.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/payment-method-dialog.svelte new file mode 100644 index 00000000..fe65a75a --- /dev/null +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/payment-method-dialog.svelte @@ -0,0 +1,151 @@ + + + + + + CATALOGO DE FORMAS DE PAGO + + +
+
+ + +
+
+ +
+ {#if loading} +
+ +
+ {:else if error} +
+

{error}

+
+ {:else} +
+ + + + + + + + + {#each filteredMethods as method, i} + handleSelect(method)} + > + + + + {/each} + {#if filteredMethods.length === 0} + + + + {/if} + +
ClaveDescripción
{method.key || ''}{method.description || ''}
+ No se encontraron resultados +
+
+ {/if} +
+ +
+ +
+
+
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-continuation.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-continuation.svelte index 6381f4c9..e3111bde 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-continuation.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-continuation.svelte @@ -3,7 +3,10 @@ import { Input } from '$lib/components/ui/input'; import { Label } from '$lib/components/ui/label'; import { Checkbox } from '$lib/components/ui/checkbox'; + import { Button } from '$lib/components/ui/button'; + import { Folder } from 'lucide-svelte'; import type { LineItem, LineDescriptions } from '$lib/api/dashboard/a76/items'; + import PaymentMethodDialog from './payment-method-dialog.svelte'; let { lineItem = $bindable(), @@ -22,6 +25,47 @@ function setHasCertificate(val: string) { lineItem.has_certificate = val === 'si'; } + + let paymentMethodDialogOpen = $state(false); + let payment_method_description = $state(''); + + // Load payment method description when payment_method exists + $effect(() => { + async function loadPaymentMethodData() { + // Si ya tiene la descripción cargada por enrichItemData, usarla + if ((lineItem as any).payment_method_description) { + payment_method_description = (lineItem as any).payment_method_description; + return; + } + + // Si tiene payment_method pero no descripción, cargarla + if (lineItem.payment_method && !payment_method_description) { + try { + const response = await fetch('/api-sveltekit/payment-methods', { + credentials: 'include' + }); + if (response.ok) { + const data = await response.json(); + const methods = data.items || data.data || data; + if (Array.isArray(methods)) { + const method = methods.find((m: any) => m.key === lineItem.payment_method); + if (method) { + payment_method_description = method.description; + } + } + } + } catch (error) { + console.error('Error loading payment method data:', error); + } + } + } + loadPaymentMethodData(); + }); + + function handlePaymentMethodSelect(method: any) { + lineItem.payment_method = method.key; + payment_method_description = method.description; + }
@@ -49,10 +93,19 @@
- + +
- +
@@ -158,3 +211,4 @@ + \ No newline at end of file diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte index 4da902cc..82d25695 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte @@ -290,7 +290,7 @@ quantity_returned: undefined, net_weight: undefined, gross_weight: undefined, - package_key: undefined, + package_id: undefined, package_quantity: undefined, package_description: undefined }, @@ -664,6 +664,53 @@ console.error('Error loading fraction data:', error); } } + + // Load package data (if needed) + const packageId = line.quantity?.package_id; + if (packageId && line.quantity) { + try { + const response = await fetch('/api-sveltekit/packages', { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + if (response.ok) { + const data = await response.json(); + const packages = data.items || data.data || data; + if (Array.isArray(packages)) { + const pkg = packages.find((p: any) => p.id === packageId); + if (pkg) { + (line.quantity as any).package_description = pkg.description_es || pkg.description_en || pkg.key; + (line.quantity as any).package_key = pkg.key; + (line.quantity as any).package_weight_unit = pkg.weight_unit || 0; + } + } + } + } catch (error) { + console.error('Error loading package data:', error); + } + } + + // Load payment method description (if needed) + if (line.payment_method) { + try { + const response = await fetch('/api-sveltekit/payment-methods', { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }); + if (response.ok) { + const data = await response.json(); + const methods = data.items || data.data || data; + if (Array.isArray(methods)) { + const method = methods.find((m: any) => m.key === line.payment_method); + if (method) { + (line as any).payment_method_description = method.description; + } + } + } + } catch (error) { + console.error('Error loading payment method data:', error); + } + } } // Normalize numeric values from strings to numbers diff --git a/frontend/src/routes/api-sveltekit/packages/+server.ts b/frontend/src/routes/api-sveltekit/packages/+server.ts new file mode 100644 index 00000000..a78d61e0 --- /dev/null +++ b/frontend/src/routes/api-sveltekit/packages/+server.ts @@ -0,0 +1,80 @@ +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ cookies, url }) => { + const token = cookies.get('access_token'); + + // Obtener company_id de la cookie + const companyId = cookies.get('active_company_id'); + + if (!companyId) { + return new Response( + JSON.stringify({ error: 'No company selected' }), + { + status: 400, + headers: { + 'Content-Type': 'application/json' + } + } + ); + } + + // Configurar la URL de la API usando las variables de entorno + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = process.env.VITE_API_URL; + // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) + apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); + } + + // Normalizar la URL + const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; + + // Get query parameters and add company_id + const searchParams = new URLSearchParams(url.search); + searchParams.set('company_id', companyId); + const queryString = searchParams.toString(); + + try { + const fetchUrl = `${baseUrl}v1/a76/packages?${queryString}`; + + const response = await fetch( + fetchUrl, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + } + ); + + const data = await response.json(); + + if (!response.ok) { + return new Response(JSON.stringify(data), { + status: response.status, + headers: { + 'Content-Type': 'application/json' + } + }); + } + + return new Response(JSON.stringify(data), { + status: 200, + headers: { + 'Content-Type': 'application/json' + } + }); + } catch (error) { + console.error('Error fetching packages:', error); + return new Response( + JSON.stringify({ error: 'Failed to fetch packages' }), + { + status: 500, + headers: { + 'Content-Type': 'application/json' + } + } + ); + } +}; diff --git a/frontend/src/routes/api-sveltekit/payment-methods/+server.ts b/frontend/src/routes/api-sveltekit/payment-methods/+server.ts new file mode 100644 index 00000000..688259a0 --- /dev/null +++ b/frontend/src/routes/api-sveltekit/payment-methods/+server.ts @@ -0,0 +1,64 @@ +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ cookies, url }) => { + const token = cookies.get('access_token'); + + // Configurar la URL de la API usando las variables de entorno + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = process.env.VITE_API_URL; + // Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR) + apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend'); + } + + // Normalizar la URL + const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; + + // Get query parameters + const searchParams = new URLSearchParams(url.search); + const queryString = searchParams.toString(); + + try { + const fetchUrl = `${baseUrl}v1/public/reference_data/payment-methods${queryString ? `?${queryString}` : ''}`; + + const response = await fetch( + fetchUrl, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + } + ); + + const data = await response.json(); + + if (!response.ok) { + return new Response(JSON.stringify(data), { + status: response.status, + headers: { + 'Content-Type': 'application/json' + } + }); + } + + return new Response(JSON.stringify(data), { + status: 200, + headers: { + 'Content-Type': 'application/json' + } + }); + } catch (error) { + console.error('Error fetching payment methods:', error); + return new Response( + JSON.stringify({ error: 'Failed to fetch payment methods' }), + { + status: 500, + headers: { + 'Content-Type': 'application/json' + } + } + ); + } +};