diff --git a/backend/api/v1/modules/a76/invoices/models.py b/backend/api/v1/modules/a76/invoices/models.py index 5aa90652..1bc8f8a0 100644 --- a/backend/api/v1/modules/a76/invoices/models.py +++ b/backend/api/v1/modules/a76/invoices/models.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import Optional, List +from typing import Optional, List, TYPE_CHECKING from sqlalchemy import ( BigInteger, Boolean, @@ -16,6 +16,9 @@ from core.database import Base from datetime import datetime from ....common.base_models import TenantScopedMixin, TimestampMixin +if TYPE_CHECKING: + from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos + class Currency(str, Enum): FOREIGN = "foreign" @@ -379,8 +382,11 @@ class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin): Integer ) # SEM / ID SEM (de SFacEntradaSM/SFacSalidaSM) - # Relationship + # Relationships header: Mapped["InvoiceHeader"] = relationship(back_populates="compliance_mx") + pedimento: Mapped[Optional["Pedimentos"]] = relationship( + foreign_keys=[pedimento_id], lazy="joined" + ) # --- 3. Financials (invoice_financials) --- diff --git a/backend/api/v1/modules/a76/invoices/schemas.py b/backend/api/v1/modules/a76/invoices/schemas.py index 2b71edc4..444f7a61 100644 --- a/backend/api/v1/modules/a76/invoices/schemas.py +++ b/backend/api/v1/modules/a76/invoices/schemas.py @@ -461,10 +461,24 @@ class InvoiceHeaderUpdate(InvoiceHeaderBase): # --- Response Schemas --- +class PedimentoBasicInfo(BaseModel): + """Basic Pedimento information for Invoice response""" + + pedimento_number: Optional[str] = None + pedimento_code: Optional[str] = None + customs_office: Optional[str] = None + license: Optional[str] = None + year: Optional[str] = None + + class Config: + from_attributes = True + + class InvoiceComplianceMxResponse(InvoiceComplianceMxBase): """Schema for Compliance MX response""" invoice_id: int + pedimento: Optional[PedimentoBasicInfo] = None class Config: from_attributes = True diff --git a/frontend/src/lib/api/dashboard/a76/invoices.ts b/frontend/src/lib/api/dashboard/a76/invoices.ts index 4ae71bfb..089f8ad0 100644 --- a/frontend/src/lib/api/dashboard/a76/invoices.ts +++ b/frontend/src/lib/api/dashboard/a76/invoices.ts @@ -15,6 +15,14 @@ export interface InvoiceComplianceMx { pedimento_r1?: number | null; pedimento_k1?: number | null; remesa?: number | null; + // Pedimento relationship data (when joined) + pedimento?: { + pedimento_number?: string; + pedimento_code?: string; + customs_office?: string; + license?: string; + year?: string; + } | null; aduana?: string | null; port_of_entry?: string | null; destination?: string | null; @@ -282,6 +290,7 @@ export interface CreateInvoiceData { } export interface UpdateInvoiceData { + id: number; // Required by backend operation_type?: OperationType | null; invoice_type?: string | null; document_type?: string | null; diff --git a/frontend/src/lib/components/dashboard/invoices/columns.ts b/frontend/src/lib/components/dashboard/invoices/columns.ts index c087fae8..a4deee43 100644 --- a/frontend/src/lib/components/dashboard/invoices/columns.ts +++ b/frontend/src/lib/components/dashboard/invoices/columns.ts @@ -4,32 +4,6 @@ import { createRawSnippet } from "svelte"; import DataTableActions from "./data-table-actions.svelte"; import type { Invoice } from '$lib/api/dashboard/a76/invoices'; -/** - * Formatea un número como moneda MXN - */ -function formatCurrencyMXN(value?: number | null): string { - if (value === null || value === undefined) return '-'; - return new Intl.NumberFormat('es-MX', { - style: 'currency', - currency: 'MXN', - minimumFractionDigits: 2, - maximumFractionDigits: 2 - }).format(value); -} - -/** - * Formatea un número como moneda USD - */ -function formatCurrencyUSD(value?: number | null): string { - if (value === null || value === undefined) return '-'; - return new Intl.NumberFormat('es-MX', { - style: 'currency', - currency: 'USD', - minimumFractionDigits: 2, - maximumFractionDigits: 2 - }).format(value); -} - /** * Formatea una fecha */ @@ -42,81 +16,11 @@ function formatDate(date?: string | null): string { }); } -/** - * Obtiene el color del badge según el tipo de operación - */ -function getOperationTypeColor(type?: string | null): string { - if (!type) return 'bg-gray-100 text-gray-800'; - return type === 'imp' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800'; -} - -/** - * Obtiene el color del badge según el semáforo fiscal - */ -function getTrafficLightColor(status?: string | null): string { - if (!status) return 'bg-gray-100 text-gray-800'; - - const statusLower = status.toLowerCase(); - if (statusLower.includes('verde') || statusLower === 'green') return 'bg-green-100 text-green-800'; - if (statusLower.includes('amarillo') || statusLower === 'yellow') return 'bg-yellow-100 text-yellow-800'; - if (statusLower.includes('rojo') || statusLower === 'red') return 'bg-red-100 text-red-800'; - - return 'bg-gray-100 text-gray-800'; -} - export function createColumns(onSuccess?: () => void): ColumnDef[] { return [ - { - accessorKey: "id", - header: "ID", - cell: ({ row }) => { - const idSnippet = createRawSnippet<[{ id: number }]>((getId) => { - const { id } = getId(); - return { - render: () => - `
#${id}
` - }; - }); - return renderSnippet(idSnippet, { id: row.original.id }); - } - }, - { - accessorKey: "operation_type", - header: "Operación", - cell: ({ row }) => { - const type = row.original.operation_type; - const colorClass = getOperationTypeColor(type); - const label = type === 'imp' ? 'IMP' : type === 'exp' ? 'EXP' : 'N/A'; - - const typeSnippet = createRawSnippet<[{ label: string; colorClass: string }]>((getType) => { - const { label, colorClass } = getType(); - return { - render: () => - ` - ${label} - ` - }; - }); - return renderSnippet(typeSnippet, { label, colorClass }); - } - }, - { - accessorKey: "invoice_type", - header: "Tipo", - cell: ({ row }) => { - const typeSnippet = createRawSnippet<[{ type?: string | null }]>((getType) => { - const { type } = getType(); - return { - render: () => - `
${type || '-'}
` - }; - }); - return renderSnippet(typeSnippet, { type: row.original.invoice_type }); - } - }, { accessorKey: "invoice_number", - header: "Número de Factura", + header: "Núm. Factura", cell: ({ row }) => { const numberSnippet = createRawSnippet<[{ number?: string | null }]>((getNumber) => { const { number } = getNumber(); @@ -127,100 +31,40 @@ export function createColumns(onSuccess?: () => void): ColumnDef[] { }); return renderSnippet(numberSnippet, { number: row.original.invoice_number }); } - }, - { - accessorKey: "project_number", - header: "Proyecto", - cell: ({ row }) => { - const projectSnippet = createRawSnippet<[{ project?: string | null }]>((getProject) => { - const { project } = getProject(); - return { - render: () => - `
${project || '-'}
` - }; - }); - return renderSnippet(projectSnippet, { project: row.original.project_number }); - } }, { accessorKey: "compliance_mx.pedimento", - header: "Pedimento", + header: "Pedimento 18", cell: ({ row }) => { const pedimento = row.original.compliance_mx?.pedimento; + const fullNumber = pedimento + ? `${pedimento.year || ''}-${pedimento.customs_office || ''}-${pedimento.license || ''}-${pedimento.pedimento_number || ''}` + : null; - const pedimentoSnippet = createRawSnippet<[{ pedimento?: string | null }]>((getPedimento) => { - const { pedimento } = getPedimento(); + const pedimentoSnippet = createRawSnippet<[{ number?: string | null }]>((getPedimento) => { + const { number } = getPedimento(); return { render: () => - `
${pedimento || '-'}
` + `${number || 'N/A'}` }; }); - return renderSnippet(pedimentoSnippet, { pedimento }); + return renderSnippet(pedimentoSnippet, { number: fullNumber }); } }, { - accessorKey: "financials.value_mn", - header: () => { - const headerSnippet = createRawSnippet(() => { - return { - render: () => `
Valor MN
` - }; - }); - return renderSnippet(headerSnippet, {}); - }, + accessorKey: "compliance_mx.remesa", + header: "Remesa", cell: ({ row }) => { - const valueMN = row.original.financials?.value_mn; + const remesa = row.original.compliance_mx?.remesa; - const valueSnippet = createRawSnippet<[{ value: string }]>((getValue) => { - const { value } = getValue(); + const remesaSnippet = createRawSnippet<[{ remesa?: number | null }]>((getRemesa) => { + const { remesa } = getRemesa(); return { render: () => - `
${value}
` + `
${remesa || '-'}
` }; }); - return renderSnippet(valueSnippet, { value: formatCurrencyMXN(valueMN) }); - } - }, - { - accessorKey: "financials.value_me", - header: () => { - const headerSnippet = createRawSnippet(() => { - return { - render: () => `
Valor ME
` - }; - }); - return renderSnippet(headerSnippet, {}); - }, - cell: ({ row }) => { - const valueME = row.original.financials?.value_me; - - const valueSnippet = createRawSnippet<[{ value: string }]>((getValue) => { - const { value } = getValue(); - return { - render: () => - `
${value}
` - }; - }); - return renderSnippet(valueSnippet, { value: formatCurrencyUSD(valueME) }); - } - }, - { - accessorKey: "traffic_light_status", - header: "Semáforo", - cell: ({ row }) => { - const status = row.original.traffic_light_status; - const colorClass = getTrafficLightColor(status); - - const statusSnippet = createRawSnippet<[{ status?: string | null; colorClass: string }]>((getStatus) => { - const { status, colorClass } = getStatus(); - return { - render: () => - ` - ${status || '-'} - ` - }; - }); - return renderSnippet(statusSnippet, { status, colorClass }); + return renderSnippet(remesaSnippet, { remesa }); } }, { @@ -237,6 +81,158 @@ export function createColumns(onSuccess?: () => void): ColumnDef[] { return renderSnippet(dateSnippet, { date: formatDate(row.original.invoice_date) }); } }, + { + accessorKey: "compliance_mx.pedimento.pedimento_code", + header: "Clave Ped.", + cell: ({ row }) => { + const pedimentoCode = row.original.compliance_mx?.pedimento?.pedimento_code; + + const claveSnippet = createRawSnippet<[{ code?: string }]>((getClave) => { + const { code } = getClave(); + return { + render: () => + `
${code || '-'}
` + }; + }); + return renderSnippet(claveSnippet, { code: pedimentoCode }); + } + }, + { + accessorKey: "document_type", + header: "Tipo Doc.", + cell: ({ row }) => { + const typeSnippet = createRawSnippet<[{ type?: string | null }]>((getType) => { + const { type } = getType(); + return { + render: () => + `
${type || '-'}
` + }; + }); + return renderSnippet(typeSnippet, { type: row.original.document_type }); + } + }, + { + accessorKey: "total_items", + header: "Total Items", + cell: ({ row }) => { + // El total de items viene del conteo de details + const totalItems = row.original.details?.length || 0; + + const itemsSnippet = createRawSnippet<[{ total: number }]>((getTotal) => { + const { total } = getTotal(); + return { + render: () => + `
${total}
` + }; + }); + return renderSnippet(itemsSnippet, { total: totalItems }); + } + }, + { + accessorKey: "financials.currency", + header: "Moneda", + cell: ({ row }) => { + const currency = row.original.financials?.currency; + + const currencySnippet = createRawSnippet<[{ currency?: string | null }]>((getCurrency) => { + const { currency } = getCurrency(); + return { + render: () => + `
${currency || '-'}
` + }; + }); + return renderSnippet(currencySnippet, { currency }); + } + }, + { + accessorKey: "financials.currency_type", + header: "Tipo Moneda", + cell: ({ row }) => { + const currencyType = row.original.financials?.currency_type; + + const typeSnippet = createRawSnippet<[{ type?: string | null }]>((getType) => { + const { type } = getType(); + return { + render: () => + `
${type || '-'}
` + }; + }); + return renderSnippet(typeSnippet, { type: currencyType }); + } + }, + { + accessorKey: "logistics.weight_type", + header: "Tipo Peso", + cell: ({ row }) => { + // weight_type está en logistics que es un array, tomamos el primer elemento + const weightType = row.original.logistics?.[0]?.weight_type; + + const weightSnippet = createRawSnippet<[{ type?: string | null }]>((getType) => { + const { type } = getType(); + return { + render: () => + `
${type || '-'}
` + }; + }); + return renderSnippet(weightSnippet, { type: weightType }); + } + }, + { + accessorKey: "is_updated", + header: "Actualizado", + cell: ({ row }) => { + const isUpdated = row.original.is_updated; + + const updatedSnippet = createRawSnippet<[{ isUpdated?: boolean | null }]>((getUpdated) => { + const { isUpdated } = getUpdated(); + const colorClass = isUpdated ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'; + const label = isUpdated ? 'Sí' : 'No'; + return { + render: () => + ` + ${label} + ` + }; + }); + return renderSnippet(updatedSnippet, { isUpdated }); + } + }, + { + accessorKey: "compliance_mx.is_mixed", + header: "Mixto", + cell: ({ row }) => { + const isMixed = row.original.compliance_mx?.is_mixed; + + const mixedSnippet = createRawSnippet<[{ isMixed?: boolean | null }]>((getMixed) => { + const { isMixed } = getMixed(); + const colorClass = isMixed ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-800'; + const label = isMixed ? 'Sí' : 'No'; + return { + render: () => + ` + ${label} + ` + }; + }); + return renderSnippet(mixedSnippet, { isMixed }); + } + }, + { + accessorKey: "related_doc_id", + header: "Doc. Relacionado", + cell: ({ row }) => { + const relDoc = row.original.related_doc_id; + + const relDocSnippet = createRawSnippet<[{ relDoc?: number | null }]>((getRelDoc) => { + const { relDoc } = getRelDoc(); + return { + render: () => + `
${relDoc || '-'}
` + }; + }); + return renderSnippet(relDocSnippet, { relDoc }); + } + }, { id: "actions", cell: ({ row }) => { diff --git a/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts b/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts index a254ab2e..ea316173 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts +++ b/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts @@ -89,7 +89,8 @@ export async function saveInvoice(options: SaveInvoiceOptions): Promise