feat: enhance invoice validation and logistics handling for improved data integrity

This commit is contained in:
2026-01-11 18:58:43 -06:00
parent 8bf0ac8969
commit 6e4e14285d
4 changed files with 83 additions and 27 deletions

View File

@@ -5,6 +5,7 @@ from api.v1.modules.a76.general_catalogs.exchange_rate.models import ExchangeRat
from api.v1.modules.a76.clients_and_providers.models import ClientProvider
from api.v1.modules.a76.customs_brokers.models import CustomsBroker
from api.v1.modules.public.reference_data.incoterms.models import Incoterm
from ....models import InvoiceComplianceMx
from api.v1.modules.a76.items.models import Item
from api.v1.modules.public.reference_data.currency_types.models import CurrencyType
from api.v1.modules.public.reference_data.customs_sections.models import CustomsSection
@@ -39,7 +40,7 @@ def validate_common(
)
if not invoice.compliance_mx.is_regime_change:
if not pedimento.operation_type == 1:
if not pedimento.operation_type == "imp":
errors.add_error(
field="compliance_mx.pedimento_id",
message="El Pedimento seleccionado no corresponde a una Importación.",
@@ -107,10 +108,24 @@ def validate_common(
)
if pedimento.pedimento_type == "consolidated":
if (
invoice.invoice_date < pedimento.pedimento_dates.entry_date
or invoice.invoice_date > pedimento.pedimento_dates.end_date
):
# Convertir invoice_date a date si es datetime para poder comparar
invoice_date = (
invoice.invoice_date.date()
if hasattr(invoice.invoice_date, "date")
else invoice.invoice_date
)
entry_date = (
pedimento.pedimento_dates.entry_date.date()
if hasattr(pedimento.pedimento_dates.entry_date, "date")
else pedimento.pedimento_dates.entry_date
)
end_date = (
pedimento.pedimento_dates.end_date.date()
if hasattr(pedimento.pedimento_dates.end_date, "date")
else pedimento.pedimento_dates.end_date
)
if invoice_date < entry_date or invoice_date > end_date:
errors.add_error(
field="invoice_date",
message=f"La Fecha de la Factura {invoice.invoice_date} no está dentro del rango de fechas del Pedimento {pedimento.customs_office}-{pedimento.license}-{pedimento.pedimento_number}.",
@@ -139,23 +154,23 @@ def validate_common(
)
duplicated_remesa = (
db.query(Pedimentos)
db.query(InvoiceComplianceMx)
.filter(
Pedimentos.remesa == invoice.compliance_mx.remesa,
Pedimentos.id != invoice.compliance_mx.pedimento_id,
Pedimentos.tenant_id == tenant_id,
Pedimentos.company_id == company_id,
InvoiceComplianceMx.remesa == invoice.compliance_mx.remesa,
InvoiceComplianceMx.tenant_id == tenant_id,
InvoiceComplianceMx.company_id == company_id,
)
.first()
)
if duplicated_remesa:
errors.add_error(
field="compliance_mx.remesa",
message="El valor de Remesa ya está asociado a otro Pedimento.",
solution=["Proporciona un valor único para Remesa"],
code="DUPLICATE_VALUE",
value=invoice.compliance_mx.remesa,
)
if duplicated_remesa and hasattr(invoice, "id"):
if invoice.id != duplicated_remesa.invoice_id:
errors.add_error(
field="compliance_mx.remesa",
message="El valor de Remesa ya está asociado a otro Pedimento.",
solution=["Proporciona un valor único para Remesa"],
code="DUPLICATE_VALUE",
value=invoice.compliance_mx.remesa,
)
else:
if not invoice.compliance_mx.is_pedimento_pending:
errors.add_error(
@@ -388,7 +403,11 @@ def validate_common(
value=invoice.financials.currency,
)
if invoice.financials.currency == "manual":
if invoice.financials.currency == "foreign":
invoice.financials.currency_type = "USD"
elif invoice.financials.currency == "local":
invoice.financials.currency_type = "MXN"
elif invoice.financials.currency == "manual":
if not invoice.financials.currency_type:
errors.add_error(
field="financials.currency_type",

View File

@@ -228,7 +228,7 @@ export interface Invoice {
enajenation_goods?: boolean | null;
compliance_mx?: InvoiceComplianceMx | null;
financials?: InvoiceFinancials | null;
logistics?: InvoiceLogistics[];
logistics?: InvoiceLogistics;
details?: InvoiceSalesDetails[];
collections?: InvoiceCollections[];
}

View File

@@ -18,6 +18,43 @@ function formatDate(date?: string | null): string {
export function createColumns(onSuccess?: () => void): ColumnDef<Invoice>[] {
return [
{
accessorKey: "operation_type",
header: "Operación",
cell: ({ row }) => {
const operationType = row.original.operation_type;
const operationSnippet = createRawSnippet<[{ type?: string | null }]>((getType) => {
const { type } = getType();
const isImport = type === 'imp';
const colorClass = isImport ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' : 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200';
const label = isImport ? 'Importación' : type === 'exp' ? 'Exportación' : '-';
return {
render: () =>
`<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${colorClass}">
${label}
</span>`
};
});
return renderSnippet(operationSnippet, { type: operationType });
}
},
{
accessorKey: "invoice_type",
header: "Tipo Factura",
cell: ({ row }) => {
const invoiceType = row.original.invoice_type;
const typeSnippet = createRawSnippet<[{ type?: string | null }]>((getType) => {
const { type } = getType();
return {
render: () =>
`<div class="text-sm font-medium">${type || '-'}</div>`
};
});
return renderSnippet(typeSnippet, { type: invoiceType });
}
},
{
accessorKey: "invoice_number",
header: "Núm. Factura",
@@ -113,7 +150,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Invoice>[] {
},
{
accessorKey: "total_items",
header: "Total Items",
header: "Total Partidas",
cell: ({ row }) => {
// El total de items viene del conteo de details
const totalItems = row.original.details?.length || 0;
@@ -165,7 +202,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Invoice>[] {
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 weightType = row.original.logistics?.weight_type;
const weightSnippet = createRawSnippet<[{ type?: string | null }]>((getType) => {
const { type } = getType();

View File

@@ -71,11 +71,11 @@
exchange_rate: invoice.financials?.exchange_rate || null, // Added exchange_rate
weight_type: 'kgs',
iva_factor: invoice.financials?.iva_factor || null,
carrier_id: invoice.logistics?.[0]?.carrier_id || null,
transport_id: invoice.logistics?.[0]?.transport_id || '',
driver_name: invoice.logistics?.[0]?.driver_name || '',
transport_type: invoice.logistics?.[0]?.transport_type || '',
transport_num: invoice.logistics?.[0]?.vehicle_num || '',
carrier_id: invoice.logistics?.carrier_id || null,
transport_id: invoice.logistics?.transport_id || '',
driver_name: invoice.logistics?.driver_name || '',
transport_type: invoice.logistics?.transport_type || '',
transport_num: invoice.logistics?.vehicle_num || '',
aduana: invoice.compliance_mx?.aduana || '',
document_type: invoice.document_type || '',
};