diff --git a/backend/api/v1/modules/a76/invoices/exports/process/sub_process/review_qty_vs_weight.py b/backend/api/v1/modules/a76/invoices/exports/process/sub_process/review_qty_vs_weight.py index 7dad03d8..3aa3b74e 100644 --- a/backend/api/v1/modules/a76/invoices/exports/process/sub_process/review_qty_vs_weight.py +++ b/backend/api/v1/modules/a76/invoices/exports/process/sub_process/review_qty_vs_weight.py @@ -61,7 +61,7 @@ def review_qty_vs_weight( continue qty = line.quantity.quantity or Decimal(0) - net_weight = getattr(line.quantity.quantity, None) or Decimal(0) + net_weight = line.quantity.net_weight or Decimal(0) if net_weight != qty: errors.add_error( diff --git a/backend/api/v1/modules/a76/invoices/imports/process/main_process.py b/backend/api/v1/modules/a76/invoices/imports/process/main_process.py index df03b322..8cc83c7f 100644 --- a/backend/api/v1/modules/a76/invoices/imports/process/main_process.py +++ b/backend/api/v1/modules/a76/invoices/imports/process/main_process.py @@ -198,7 +198,7 @@ def _update_invoice_totals(invoice: InvoiceHeader) -> None: # Marcar la factura como procesada invoice.status = InvoiceStatus.PROCESSED - invoice.party_count = len(invoice.financials.__dict__) # se sobreescribirá con el conteo real + invoice.party_count = len(lines) # TODO: SSisGen:ActSeguridad = 1 → invoice.updated_by = current_user # TODO: SSisGen:CalValBaseTCPed = 1 → diff --git a/backend/api/v1/modules/a76/invoices/services.py b/backend/api/v1/modules/a76/invoices/services.py index 63304a24..a9871ad6 100644 --- a/backend/api/v1/modules/a76/invoices/services.py +++ b/backend/api/v1/modules/a76/invoices/services.py @@ -10,6 +10,7 @@ from .imports.validators.update import validate_update as validate_update_import from .exports.validators.create import validate_create as validate_create_export from .exports.validators.update import validate_update as validate_update_export from .common.common_validators import invoice_exists +from api.v1.modules.a76.items.models import LineItem from . import models, schemas @@ -166,6 +167,25 @@ class InvoiceService: total = query.count() items = query.offset(skip).limit(limit).all() + + # Keep party_count aligned with the real number of line items. + # This avoids stale values stored in invoice_header.party_count. + if items: + invoice_ids = [inv.id for inv in items] + counts = ( + db.query(LineItem.invoice_id, func.count(LineItem.id)) + .filter( + LineItem.invoice_id.in_(invoice_ids), + LineItem.tenant_id == tenant_id, + LineItem.company_id == company_id, + ) + .group_by(LineItem.invoice_id) + .all() + ) + count_map = {invoice_id: int(count) for invoice_id, count in counts} + for inv in items: + inv.party_count = count_map.get(inv.id, 0) + return items, total @staticmethod diff --git a/backend/api/v1/modules/a76/items/exports/validators/create.py b/backend/api/v1/modules/a76/items/exports/validators/create.py index 9c19d6bf..4446bc4c 100644 --- a/backend/api/v1/modules/a76/items/exports/validators/create.py +++ b/backend/api/v1/modules/a76/items/exports/validators/create.py @@ -248,18 +248,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.lower() == "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.lower() == "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.lower() == "kgs": # El peso capturado está en kilos line.quantity.net_weight = net_weight_input else: @@ -289,7 +289,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.lower() == "kgs": line.quantity.gross_weight = line.quantity.net_weight + ( package_weight_unit * package_quantity ) @@ -299,7 +299,7 @@ def validate_create( ) else: # Convertir peso bruto capturado según tipo de factura - if invoice_weight_type == "KGS": + if invoice_weight_type.lower() == "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/exports/validators/update.py b/backend/api/v1/modules/a76/items/exports/validators/update.py index 4f8e2c9d..6f9ad8a1 100644 --- a/backend/api/v1/modules/a76/items/exports/validators/update.py +++ b/backend/api/v1/modules/a76/items/exports/validators/update.py @@ -95,21 +95,19 @@ 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.lower() == "kgs": line.quantity.net_weight = net_weight_input else: # libras, convertir a kilos line.quantity.net_weight = net_weight_input / Decimal("2.204624") else: # Mantener peso existente - line.quantity.net_weight = existing_line.quantity.net_weight - - print(f"After weight conversion: net_weight={line.quantity.net_weight}, gross_weight={line.quantity.gross_weight}, weight_type={invoice_weight_type}") + line.quantity.net_weight = existing_line.quantity.net_weight # Convertir peso bruto si se proporcionó if line.quantity.gross_weight is not None: gross_weight_input = line.quantity.gross_weight - if invoice_weight_type == "KGS": + if invoice_weight_type.lower() == "kgs": line.quantity.gross_weight = gross_weight_input else: # libras, convertir a kilos line.quantity.gross_weight = gross_weight_input / Decimal("2.204624") diff --git a/backend/api/v1/modules/a76/items/imports/validators/create.py b/backend/api/v1/modules/a76/items/imports/validators/create.py index 14780846..7dc00b1c 100644 --- a/backend/api/v1/modules/a76/items/imports/validators/create.py +++ b/backend/api/v1/modules/a76/items/imports/validators/create.py @@ -232,18 +232,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.lower() == "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.lower() == "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.lower() == "kgs": # El peso capturado está en kilos line.quantity.net_weight = net_weight_input else: @@ -273,7 +273,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.lower() == "kgs": line.quantity.gross_weight = line.quantity.net_weight + ( package_weight_unit * package_quantity ) @@ -283,7 +283,7 @@ def validate_create( ) else: # Convertir peso bruto capturado según tipo de factura - if invoice_weight_type == "KGS": + if invoice_weight_type.lower() == "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/validators/update.py b/backend/api/v1/modules/a76/items/imports/validators/update.py index c1343a0d..c5bd5e7a 100644 --- a/backend/api/v1/modules/a76/items/imports/validators/update.py +++ b/backend/api/v1/modules/a76/items/imports/validators/update.py @@ -94,21 +94,19 @@ 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.lower() == "kgs": line.quantity.net_weight = net_weight_input else: # libras, convertir a kilos line.quantity.net_weight = net_weight_input / Decimal("2.204624") else: # Mantener peso existente - line.quantity.net_weight = existing_line.quantity.net_weight - - print(f"After weight conversion: net_weight={line.quantity.net_weight}, gross_weight={line.quantity.gross_weight}, weight_type={invoice_weight_type}") + line.quantity.net_weight = existing_line.quantity.net_weight # Convertir peso bruto si se proporcionó if line.quantity.gross_weight is not None: gross_weight_input = line.quantity.gross_weight - if invoice_weight_type == "KGS": + if invoice_weight_type.lower() == "kgs": line.quantity.gross_weight = gross_weight_input else: # libras, convertir a kilos line.quantity.gross_weight = gross_weight_input / Decimal("2.204624") diff --git a/frontend/src/lib/api/dashboard/a76/invoices.ts b/frontend/src/lib/api/dashboard/a76/invoices.ts index 371a8159..d3b7d812 100644 --- a/frontend/src/lib/api/dashboard/a76/invoices.ts +++ b/frontend/src/lib/api/dashboard/a76/invoices.ts @@ -232,13 +232,14 @@ export interface Invoice { download_substance?: boolean | null; download_class?: boolean | null; download_def?: boolean | null; + total_items?: number | null; payment_terms?: string | null; handling_fees?: number | null; option_iv18?: string | null; enajenation_goods?: boolean | null; compliance_mx?: InvoiceComplianceMx | null; financials?: InvoiceFinancials | null; - logistics?: InvoiceLogistics[]; + logistics?: InvoiceLogistics | null; details?: InvoiceSalesDetails[]; collections?: InvoiceCollections[]; // Client-side only properties diff --git a/frontend/src/lib/components/dashboard/invoices/columns.ts b/frontend/src/lib/components/dashboard/invoices/columns.ts index 9c4ffd83..7821e55f 100644 --- a/frontend/src/lib/components/dashboard/invoices/columns.ts +++ b/frontend/src/lib/components/dashboard/invoices/columns.ts @@ -6,7 +6,19 @@ import { getInvoiceTypeColor } from "$lib/utils"; function formatDate(date?: string | null): string { if (!date) return '-'; - return new Date(date).toLocaleDateString('es-MX', { + // Avoid timezone shifts (e.g. showing one day earlier) by parsing as local date. + const raw = String(date); + const ymd = raw.includes('T') ? raw.split('T')[0] : raw; + const parts = ymd.split('-').map(Number); + if (parts.length === 3 && parts.every((n) => Number.isFinite(n))) { + const [year, month, day] = parts; + return new Date(year, month - 1, day).toLocaleDateString('es-MX', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }); + } + return new Date(raw).toLocaleDateString('es-MX', { year: 'numeric', month: '2-digit', day: '2-digit' @@ -243,7 +255,10 @@ export function createColumns( accessorKey: "total_items", header: "Total Partidas", cell: ({ row }) => { - const totalItems = row.original.details?.length || 0; + const totalItems = (row.original as Invoice & { total_items?: number | null }).total_items + ?? row.original.party_count + ?? row.original.details?.length + ?? 0; const itemsSnippet = createRawSnippet<[{ total: number }]>((getTotal) => { const { total } = getTotal(); @@ -291,7 +306,7 @@ export function createColumns( accessorKey: "logistics.weight_type", header: "Tipo Peso", cell: ({ row }) => { - const weightType = row.original.logistics?.[0]?.weight_type; + const weightType = row.original.logistics?.weight_type; const weightSnippet = createRawSnippet<[{ type?: string | null }]>((getType) => { const { type } = getType(); @@ -303,28 +318,6 @@ export function createColumns( return renderSnippet(weightSnippet, { type: weightType }); } }, - { - accessorKey: "status", - header: "Actualizado", - cell: ({ row }) => { - // status can be 'processed' | 'pending' | 'reversed' (string) or legacy boolean - const s = row.original.status; - const isprocessed = s === "processed" || s === true; - - const processedSnippet = createRawSnippet<[{ isprocessed?: boolean | null }]>((getprocessed) => { - const { isprocessed } = getprocessed(); - const colorClass = isprocessed ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'; - const label = isprocessed ? 'Sí' : 'No'; - return { - render: () => - ` - ${label} - ` - }; - }); - return renderSnippet(processedSnippet, { isprocessed }); - } - }, { accessorKey: "compliance_mx.is_mixed", header: "Mixto", diff --git a/frontend/src/lib/components/dashboard/invoices/data-table.svelte b/frontend/src/lib/components/dashboard/invoices/data-table.svelte index cb2c4dbc..29b7fb10 100644 --- a/frontend/src/lib/components/dashboard/invoices/data-table.svelte +++ b/frontend/src/lib/components/dashboard/invoices/data-table.svelte @@ -83,9 +83,21 @@ {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} + {@const headerList = headerGroup.headers} + {@const lastHeaderColId = headerList[headerList.length - 1]?.column.id} - {#each headerGroup.headers as header (header.id)} - + {#each headerList as header (header.id)} + {@const colId = header.column.id} + {#if !header.isPlaceholder} {#each table.getRowModel().rows as row (row.id)} + {@const visibleCells = row.getVisibleCells()} + {@const lastCellColId = visibleCells[visibleCells.length - 1]?.column.id} onRowClick && onRowClick(row.original)} > - {#each row.getVisibleCells() as cell (cell.id)} - + {#each visibleCells as cell (cell.id)} + {@const colId = cell.column.id} + {/each} 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 4dd63e0d..ea26ad84 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 @@ -69,7 +69,7 @@ editingItem.fa_data.omit_annex31 = false; } if (editingItem.fa_data.discharge === undefined) { - editingItem.fa_data.discharge = false; + editingItem.fa_data.discharge = true; } } }); @@ -271,7 +271,7 @@
{ editingItem.fa_data = editingItem.fa_data || {}; editingItem.fa_data.discharge = v === 'si'; @@ -364,7 +364,7 @@
{ editingItem.fa_data = editingItem.fa_data || {}; editingItem.fa_data.discharge = v === 'si'; 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 5d76cde3..b6dcf4e4 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 @@ -102,10 +102,12 @@ const invoiceSystem = $derived(invoice?.system || 'scaii'); const itemVisibility = $derived.by(() => getVisibility(invoiceType, operationType)); const showCrTrackingHeader = $derived(itemVisibility.showCrTrackingHeader); - const showTrackingHeaderColumns = $derived(operationType !== 1 && showCrTrackingHeader); + /** Debe coincidir con el orden de celdas en cada rama del tbody (exportación ≠ importación genérica ≠ REP). */ const emptyStateColspan = $derived.by(() => { if (operationType === 1) return 11; - return showTrackingHeaderColumns ? 12 : 10; + if (showCrTrackingHeader) return 12; + if (invoiceType === 'REP' || invoiceType === 'REPAR') return 13; + return 10; }); const invoiceLabel = $derived.by(() => { if (invoice?.invoice_number) return `Factura ${invoice.invoice_number}`; @@ -333,7 +335,7 @@ search_line: undefined, search_type: undefined, movement_type_import: undefined, - down_equipment: false, + own_equipment: false, omit_annex31: false }, series: [] @@ -1011,6 +1013,28 @@ // Cerrar el sheet showItemSheet = false; } + + /** Sticky sin bordes extra (evitan desalinear thead/tbody en tablas auto-layout). */ + const STICKY_LINE_HEAD = + 'sticky left-0 z-40 bg-background shadow-[3px_0_8px_-4px_rgba(0,0,0,0.12)] dark:shadow-[3px_0_8px_-4px_rgba(0,0,0,0.35)]'; + const STICKY_ACTIONS_HEAD = + 'sticky right-0 z-40 w-[104px] min-w-[104px] bg-background text-right shadow-[-3px_0_8px_-4px_rgba(0,0,0,0.12)] dark:shadow-[-3px_0_8px_-4px_rgba(0,0,0,0.35)]'; + + function stickyLineCellClass(itemId: string) { + const focused = focusedLine?.id === itemId; + return [ + 'sticky left-0 z-30 shadow-[3px_0_8px_-4px_rgba(0,0,0,0.12)] dark:shadow-[3px_0_8px_-4px_rgba(0,0,0,0.35)]', + focused ? 'bg-muted' : 'bg-background group-hover/item-row:bg-muted/50' + ].join(' '); + } + + function stickyActionsCellClass(itemId: string) { + const focused = focusedLine?.id === itemId; + return [ + 'sticky right-0 z-30 w-[104px] min-w-[104px] text-right shadow-[-3px_0_8px_-4px_rgba(0,0,0,0.12)] dark:shadow-[-3px_0_8px_-4px_rgba(0,0,0,0.35)]', + focused ? 'bg-muted' : 'bg-background group-hover/item-row:bg-muted/50' + ].join(' '); + }
@@ -1052,20 +1076,54 @@ - Línea - {#if showTrackingHeaderColumns} + Línea + {#if operationType === 1} Factura Impo Línea + P/S + Cant. Importada + Clase + Número Parte + Descripción + Contiene Subpartida + Partida Principal + Acciones + {:else if showCrTrackingHeader} + Factura Impo + Línea + P/S + Clase + Descripcion Clase + Cant. Importada + U.M. + Preferencia + Contiene Subpartida + Partida Principal + Acciones + {:else if invoiceType === 'REP' || invoiceType === 'REPAR'} + Factura Impo + Línea + P/S + Clase + Número Parte + Descripcion Clase + Cant. Importada + U.M. + Preferencia + Contiene Subpartida + Partida Principal + Acciones + {:else} + P/S + Clase + Descripcion Clase + Cant. Importada + U.M. + Preferencia + Contiene Subpartida + Partida Principal + Acciones {/if} - P/S - Clase - Descripcion Clase - Cant. Importada - U.M. - Preferencia - Contiene Subpartida - Partida Principal - Acciones @@ -1082,13 +1140,13 @@ {#each displayedItems as item (item.id)} handleRowClick(item)} - class="cursor-pointer transition-colors hover:bg-muted/50 {focusedLine?.id === + class="group/item-row cursor-pointer transition-colors hover:bg-muted/50 {focusedLine?.id === item.id ? 'bg-muted ring-1 ring-primary/20 ring-inset' : ''}" > {#if operationType === 1} - {item.line_number} + {item.line_number} {item.fa_data?.search_invoice || '-'} {item.fa_data?.search_line || '-'} {item.is_subitem ? 'S' : 'P'} @@ -1104,7 +1162,7 @@ {item.fa_data?.contains_subitems ? 'Sí' : 'No'} {item.warehouse || '-'} {:else if showCrTrackingHeader} - {item.line_number} + {item.line_number} {item.fa_data?.search_invoice || '-'} {item.fa_data?.search_line || '-'} {item.is_subitem ? 'S' : 'P'} @@ -1121,7 +1179,7 @@ {item.fa_data?.contains_subitems ? 'Sí' : 'No'} {item.warehouse || '-'} {:else if invoiceType === 'REP' || invoiceType === 'REPAR'} - {item.line_number} + {item.line_number} {item.fa_data?.search_invoice || '-'} {item.fa_data?.search_line || '-'} {item.is_subitem ? 'S' : 'P'} @@ -1139,7 +1197,7 @@ {item.fa_data?.contains_subitems ? 'Sí' : 'No'} {item.warehouse || '-'} {:else} - {item.line_number} + {item.line_number} {item.is_subitem ? 'S' : 'P'} {item.class_code || '-'} {item.class_description || '-'} @@ -1149,12 +1207,28 @@ {item.fa_data?.contains_subitems ? 'Sí' : 'No'} {item.warehouse || '-'} {/if} - +
- -
diff --git a/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte b/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte index 0368c8b6..af17d17f 100644 --- a/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte +++ b/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte @@ -1,5 +1,6 @@