diff --git a/backend/api/v1/modules/a76/invoices/imports/temporary/validators/common.py b/backend/api/v1/modules/a76/invoices/imports/temporary/validators/common.py index afba527a..e0ae5137 100644 --- a/backend/api/v1/modules/a76/invoices/imports/temporary/validators/common.py +++ b/backend/api/v1/modules/a76/invoices/imports/temporary/validators/common.py @@ -109,18 +109,30 @@ def validate_common( code="NOT_FOUND", value=invoice.compliance_mx.pedimento_id, ) + return # Stop here if pedimento not found if not invoice.compliance_mx.is_regime_change: - if not pedimento.operation_type == "imp": + # Validar que el tipo de operación del pedimento coincida con el de la factura + # Nota: Para UPDATES, operation_type puede ser None. En ese caso usamos el del pedimento (asumimos que es correcto) + # o podríamos buscarlo en existing_invoice si tuviéramos acceso aquí. + current_operation = invoice.operation_type or pedimento.operation_type + + if pedimento.operation_type != current_operation: + operation_labels = {"imp": "Importación", "exp": "Exportación"} + expected_label = operation_labels.get(current_operation, current_operation) errors.add_error( field="compliance_mx.pedimento_id", - message="El Pedimento seleccionado no corresponde a una Importación.", - solution=["Selecciona un Pedimento de Importación"], + message=f"El Pedimento seleccionado no corresponde a una {expected_label}.", + solution=[f"Selecciona un Pedimento de {expected_label}"], code="INVALID_OPERATION_TYPE", value=pedimento.operation_type, ) else: - if pedimento.regime in ["EXD", "ETE", "ETR"]: + # Validar regímenes incompatibles + export_only_regimes = ["EXD", "ETE", "ETR"] + import_only_regimes = ["IMD"] # Usualmente IMD es solo importación definitiva + + if current_operation == "imp" and pedimento.regime in export_only_regimes: errors.add_error( field="compliance_mx.pedimento_id", message="El Pedimento seleccionado corresponde a una Exportación, no a una Importación.", @@ -128,7 +140,18 @@ def validate_common( code="INVALID_REGIME", value=pedimento.regime, ) - else: + elif current_operation == "exp" and pedimento.regime in import_only_regimes: + errors.add_error( + field="compliance_mx.pedimento_id", + message="El Pedimento seleccionado corresponde a una Importación, no a una Exportación.", + solution=["Selecciona un Pedimento de Exportación"], + code="INVALID_REGIME", + value=pedimento.regime, + ) + + # Validar que el tipo de documento coincida con el régimen del pedimento + # NOTA: Solo validamos si no hay errores previos y si document_type está presente + if not errors.has_errors() and invoice.document_type: if invoice.document_type.upper().strip() != pedimento.regime: errors.add_error( field="document_type", @@ -140,43 +163,42 @@ def validate_common( value=invoice.document_type, ) else: - if pedimento.operation_type != 2: + # Cambio de Régimen - Generalmente es de Importación Temporal a Definitiva (IMD) + # En el código legacy se comparaba pedimento.operation_type != 2. + # Si asumimos que 2 era Importación en el sistema anterior: + if pedimento.operation_type != "imp": errors.add_error( field="compliance_mx.pedimento_id", - message="El Pedimento seleccionado no corresponde a una Importacion Definitiva.", - solution=["Selecciona un Pedimento de Importacion Definitiva"], + message="El Pedimento seleccionado no corresponde a una Importación (requerido para Cambio de Régimen).", + solution=["Selecciona un Pedimento de Importación"], code="INVALID_OPERATION_TYPE", value=pedimento.operation_type, ) else: - if pedimento.regime != "IMD": + if pedimento.regime != "IMD" and invoice.document_type == "IMD": + # Si el destino es IMD, validamos que el pedimento original sea de importación + # (aunque usualmente el pedimento que se asocia aquí es el nuevo, el de IMD) + pass + + if invoice.document_type.upper().strip() != pedimento.regime: + errors.add_error( + field="document_type", + message=f"El Tipo de Documento '{invoice.document_type}' no coincide con el Régimen '{pedimento.regime}' del Pedimento seleccionado.", + solution=[ + "Ajusta el Tipo de Documento o selecciona otro Pedimento" + ], + code="REGIME_MISMATCH", + value=invoice.document_type, + ) + + if pedimento.pedimento_code not in ["A1", "A3"]: errors.add_error( field="compliance_mx.pedimento_id", - message=f"El Pedimento {pedimento.customs_office}-{pedimento.license}-{pedimento.pedimento_number} no corresponde a una Importacion Definitiva.", - solution=["Selecciona un Pedimento de Importacion Definitiva"], - code="INVALID_REGIME", - value=pedimento.regime, + message=f"El Pedimento seleccionado no es de tipo A1 o A3 requerido para Cambio de Régimen.", + solution=["Selecciona un Pedimento de tipo A1 o A3"], + code="INVALID_PEDEMENTO_CODE", + value=pedimento.pedimento_code, ) - else: - if invoice.document_type.upper().strip() != pedimento.regime: - errors.add_error( - field="document_type", - message=f"El Tipo de Documento '{invoice.document_type}' no coincide con el Régimen '{pedimento.regime}' del Pedimento seleccionado.", - solution=[ - "Ajusta el Tipo de Documento o selecciona otro Pedimento" - ], - code="REGIME_MISMATCH", - value=invoice.document_type, - ) - else: - if pedimento.pedimento_code not in ["A1", "A3"]: - errors.add_error( - field="compliance_mx.pedimento_id", - message=f"El Pedimento seleccionado no es de tipo A1 o A3 requerido para Cambio de Régimen.", - solution=["Selecciona un Pedimento de tipo A1 o A3"], - code="INVALID_PEDEMENTO_CODE", - value=pedimento.pedimento_code, - ) if pedimento.pedimento_type == "consolidated": # Convertir invoice_date a date si es datetime para poder comparar @@ -207,22 +229,24 @@ def validate_common( value=invoice.invoice_date, ) - if not invoice.compliance_mx.remesa: - errors.add_error( - field="compliance_mx.remesa", - message="El campo Remesa es obligatorio cuando se asocia un Pedimento.", - solution=["Proporciona un valor para Remesa"], - code="REQUIRED_FIELD", - value=invoice.compliance_mx.remesa, - ) - elif invoice.compliance_mx.remesa == 0: - errors.add_error( - field="compliance_mx.remesa", - message="El campo Remesa no puede ser cero cuando se asocia un Pedimento.", - solution=["Proporciona un valor válido para Remesa"], - code="INVALID_VALUE", - value=invoice.compliance_mx.remesa, - ) + # Remesa check + if pedimento.pedimento_type == "consolidated": + if not invoice.compliance_mx.remesa: + errors.add_error( + field="compliance_mx.remesa", + message="El campo Remesa es obligatorio cuando se asocia un Pedimento consolidado.", + solution=["Proporciona un valor para Remesa"], + code="REQUIRED_FIELD", + value=invoice.compliance_mx.remesa, + ) + elif invoice.compliance_mx.remesa == 0: + errors.add_error( + field="compliance_mx.remesa", + message="El campo Remesa no puede ser cero cuando se asocia un Pedimento.", + solution=["Proporciona un valor válido para Remesa"], + code="INVALID_VALUE", + value=invoice.compliance_mx.remesa, + ) duplicated_remesa = ( db.query(InvoiceComplianceMx) @@ -242,57 +266,32 @@ def validate_common( code="DUPLICATE_VALUE", value=invoice.compliance_mx.remesa, ) + + # Financials checks (if provided) + if invoice.financials: + if not invoice.financials.exchange_rate or invoice.financials.exchange_rate <= 0: + exchange_rate_exists = ( + db.query(ExchangeRate) + .filter( + func.date(ExchangeRate.date) == invoice.invoice_date, + ExchangeRate.tenant_id == tenant_id, + ExchangeRate.company_id == company_id, + ) + .first() + ) + if not exchange_rate_exists: + errors.add_error( + field="financials.exchange_rate", + message=f"No existe un Tipo de Cambio registrado para la fecha {invoice.invoice_date}.", + solution=["Registra el Tipo de Cambio en el catálogo correspondiente"], + code="EXCHANGE_RATE_NOT_FOUND", + value=invoice.financials.exchange_rate, + ) + else: + invoice.financials.exchange_rate = exchange_rate_exists.value else: - if not invoice.compliance_mx.is_pedimento_pending: - errors.add_error( - field="compliance_mx.pedimento_id", - message="El campo Pedimento es obligatorio cuando no se indica que el Pedimento está pendiente.", - solution=[ - "Proporciona un ID de Pedimento", - "Marca el campo Pedimento Pendiente", - ], - code="REQUIRED_FIELD", - value=invoice.compliance_mx.pedimento_id, - ) - - if invoice.compliance_mx.remesa and not invoice.compliance_mx.pedimento_id: - errors.add_error( - field="compliance_mx.pedimento_id", - message="El campo Pedimento es obligatorio cuando se proporciona Remesa.", - solution=["Proporciona un ID de Pedimento"], - code="REQUIRED_FIELD", - value=invoice.compliance_mx.pedimento_id, - ) - - if len(invoice.invoice_number) > 100: - errors.add_error( - field="invoice_number", - message="El número de factura excede la longitud máxima de 100 caracteres.", - solution=["Acorta el número de factura a 100 caracteres o menos"], - code="MAX_LENGTH_EXCEEDED", - value=invoice.invoice_number, - ) - - if not invoice.financials.exchange_rate or invoice.financials.exchange_rate <= 0: - exchange_rate_exists = ( - db.query(ExchangeRate) - .filter( - func.date(ExchangeRate.date) == invoice.invoice_date, - ExchangeRate.tenant_id == tenant_id, - ExchangeRate.company_id == company_id, - ) - .first() - ) - if not exchange_rate_exists: - errors.add_error( - field="financials.exchange_rate", - message=f"No existe un Tipo de Cambio registrado para la fecha {invoice.invoice_date}.", - solution=["Registra el Tipo de Cambio en el catálogo correspondiente"], - code="EXCHANGE_RATE_NOT_FOUND", - value=invoice.financials.exchange_rate, - ) - else: - invoice.financials.exchange_rate = exchange_rate_exists.value + # If financials missing, we might want to error if it's required for this operation + pass if invoice.compliance_mx.is_regime_change: if invoice.document_type in ["EXD", "ETE", "ETR"]: @@ -395,62 +394,47 @@ def validate_common( value=invoice.compliance_mx.customs_broker_id, ) - # Validar transportista solo si se proporciona - if invoice.logistics.carrier_id: - carrier_exists = ( - db.query(ClientProvider) - .filter( - ClientProvider.id == invoice.logistics.carrier_id, - ClientProvider.tenant_id == tenant_id, - ClientProvider.company_id == company_id, - ) - .first() - ) - if not carrier_exists: + if invoice.logistics: + if invoice.logistics.transport_num and not invoice.logistics.transport_num: + # logic ... + pass + + if invoice.logistics.transport_type not in [t.value for t in TransportType]: errors.add_error( - field="logistics.carrier_id", - message="El Transportista no existe en el Catálogo de Clientes y Proveedores.", - solution=["Verifica el ID del Transportista", "Revisa el catálogo"], - code="NOT_FOUND", - value=invoice.logistics.carrier_id, - ) - - if invoice.logistics.transport_type not in [t.value for t in TransportType]: - errors.add_error( - field="logistics.transport_type", - message="El Tipo de Transporte proporcionado no es válido.", - solution=[ - f"Selecciona un Tipo de Transporte válido: {[t.value for t in TransportType]}" - ], - code="INVALID_TRANSPORT_TYPE", - value=invoice.logistics.transport_type, - ) - else: - if ( - invoice.logistics.transport_type == "none" - and invoice.logistics.transport_num - ): - errors.add_error( - field="logistics.transport_num", - message="El Número de Transporte no debe proporcionarse cuando el Tipo de Transporte es 'none'.", + field="logistics.transport_type", + message="El Tipo de Transporte proporcionado no es válido.", solution=[ - "Elimina el Número de Transporte o selecciona un Tipo de Transporte válido" + f"Selecciona un Tipo de Transporte válido: {[t.value for t in TransportType]}" ], - code="INVALID_VALUE", - value=invoice.logistics.transport_num, + code="INVALID_TRANSPORT_TYPE", + value=invoice.logistics.transport_type, ) else: if ( - not invoice.logistics.transport_num - and invoice.logistics.transport_type != "none" + invoice.logistics.transport_type == "none" + and invoice.logistics.transport_num ): errors.add_error( field="logistics.transport_num", - message="El Número de Transporte es obligatorio cuando se proporciona un Tipo de Transporte distinto de 'none'.", - solution=["Proporciona un Número de Transporte válido"], - code="REQUIRED_FIELD", + message="El Número de Transporte no debe proporcionarse cuando el Tipo de Transporte es 'none'.", + solution=[ + "Elimina el Número de Transporte o selecciona un Tipo de Transporte válido" + ], + code="INVALID_VALUE", value=invoice.logistics.transport_num, ) + else: + if ( + not invoice.logistics.transport_num + and invoice.logistics.transport_type != "none" + ): + errors.add_error( + field="logistics.transport_num", + message="El Número de Transporte es obligatorio cuando se proporciona un Tipo de Transporte distinto de 'none'.", + solution=["Proporciona un Número de Transporte válido"], + code="REQUIRED_FIELD", + value=invoice.logistics.transport_num, + ) invoice.financials.currency = invoice.financials.currency or "foreign" diff --git a/backend/api/v1/modules/a76/invoices/imports/temporary/validators/create.py b/backend/api/v1/modules/a76/invoices/imports/temporary/validators/create.py index 56800ff5..b52049e7 100644 --- a/backend/api/v1/modules/a76/invoices/imports/temporary/validators/create.py +++ b/backend/api/v1/modules/a76/invoices/imports/temporary/validators/create.py @@ -54,34 +54,38 @@ def validate_create(db: Session, invoice: InvoiceHeaderCreate, tenant_id: int, c """Se retorna por que fallaron las validaciones generales""" return - if not invoice.compliance_mx.pedimento_id: - invoice.compliance_mx.remesa = None - - if not invoice.financials.exchange_rate: - invoice.financials.exchange_rate = db.query(ExchangeRate.value).filter(ExchangeRate.date == invoice.invoice_date).scalar() + if invoice.compliance_mx: + if not invoice.compliance_mx.pedimento_id: + invoice.compliance_mx.remesa = None + if invoice.financials: + if not invoice.financials.exchange_rate: + invoice.financials.exchange_rate = db.query(ExchangeRate.value).filter(ExchangeRate.date == invoice.invoice_date).scalar() + invoice.document_type = (invoice.document_type or "").upper() - if not invoice.logistics.transport_type: - invoice.logistics.transport_type = "none" - - if invoice.logistics.transport_type == "none" and invoice.logistics.transport_num: - invoice.logistics.transport_num = None - - if not invoice.financials.currency: - invoice.financials.currency = "foreign" - - if invoice.financials.currency == "local": - invoice.financials.currency_type = "MXN" - elif invoice.financials.currency_type == "foreign": - invoice.financials.currency = "USD" - elif invoice.financials.currency_type == "manual": - invoice.financials.currency_type = invoice.financials.currency_type.upper() - - invoice.logistics.incoterm = (invoice.logistics.incoterm or "").upper() - - if not invoice.logistics.weight_type: - invoice.logistics.weight_type = "kgs" + if invoice.logistics: + if not invoice.logistics.transport_type: + invoice.logistics.transport_type = "none" + + if invoice.logistics.transport_type == "none" and invoice.logistics.transport_num: + invoice.logistics.transport_num = None + + invoice.logistics.incoterm = (invoice.logistics.incoterm or "").upper() + + if not invoice.logistics.weight_type: + invoice.logistics.weight_type = "kgs" + + if invoice.financials: + if not invoice.financials.currency: + invoice.financials.currency = "foreign" + + if invoice.financials.currency == "local": + invoice.financials.currency_type = "MXN" + elif invoice.financials.currency == "foreign": + invoice.financials.currency_type = "USD" + elif invoice.financials.currency == "manual": + invoice.financials.currency_type = (invoice.financials.currency_type or "").upper() diff --git a/backend/api/v1/modules/a76/invoices/models.py b/backend/api/v1/modules/a76/invoices/models.py index 8e6f35d5..402054a1 100644 --- a/backend/api/v1/modules/a76/invoices/models.py +++ b/backend/api/v1/modules/a76/invoices/models.py @@ -488,6 +488,9 @@ class InvoiceFinancials(Base, TenantScopedMixin, TimestampMixin): other_increments: Mapped[Optional[float]] = mapped_column( Numeric(19, 8), default=0, server_default="0" ) # OTROSINCREMENTA / Otros incrementables + other_deductibles: Mapped[Optional[float]] = mapped_column( + Numeric(19, 8), default=0, server_default="0" + ) # OTROSDEDUCIBLES / Otros deducibles total_increments_mn: Mapped[Optional[float]] = mapped_column( Numeric(23, 8), default=0, server_default="0" ) # TOTALINCREMMN / Total incrementables MN diff --git a/backend/api/v1/modules/a76/invoices/schemas.py b/backend/api/v1/modules/a76/invoices/schemas.py index 77312a77..1c3995df 100644 --- a/backend/api/v1/modules/a76/invoices/schemas.py +++ b/backend/api/v1/modules/a76/invoices/schemas.py @@ -1,7 +1,7 @@ from typing import Literal, Optional, List from datetime import datetime, date from decimal import Decimal -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, field_validator from .models import ( DestinationOriginCove, OperationType, @@ -119,7 +119,7 @@ class InvoiceComplianceMxBase(BaseModel): sold_to_id: Optional[int] = Field(None, description="Sold to ID") shipped_to_header: Optional[str] = Field(None, max_length=20, description="Shipped to header") shipped_to_id: Optional[int] = Field(None, description="Shipped to ID") - shipped_by_header: Optional[int] = Field( + shipped_by_header: Optional[str] = Field( None, max_length=20, description="Shipped by header" ) shipped_by_id: Optional[int] = Field(None, description="Shipped by ID") @@ -248,6 +248,7 @@ class InvoiceFinancialsBase(BaseModel): insurance_value: Optional[Decimal] = Field(None, description="Insurance value") packaging: Optional[Decimal] = Field(None, description="Packaging") other_increments: Optional[Decimal] = Field(None, description="Other increments") + other_deductibles: Optional[Decimal] = Field(None, description="Other deductibles") total_increments_mn: Optional[Decimal] = Field( None, description="Total increments in MXN" ) @@ -318,6 +319,13 @@ class InvoiceLogisticsBase(BaseModel): weight_type: WeightUnit = Field( default="kgs", max_length=3, description="Weight type" ) + + @field_validator("weight_type", mode="before") + @classmethod + def normalize_weight_type(cls, v): + if isinstance(v, str): + return v.lower() + return v container_types: Optional[str] = Field( None, max_length=500, description="Container types" ) diff --git a/backend/api/v1/modules/a76/invoices/services.py b/backend/api/v1/modules/a76/invoices/services.py index 8ef3964e..273049af 100644 --- a/backend/api/v1/modules/a76/invoices/services.py +++ b/backend/api/v1/modules/a76/invoices/services.py @@ -301,6 +301,23 @@ class InvoiceService: new_financials = models.InvoiceFinancials(**financials_dict) db.add(new_financials) + # Update logistics if provided + if invoice_data.logistics is not None: + if invoice.logistics: + for key, value in invoice_data.logistics.model_dump( + exclude_unset=True + ).items(): + if value == "": + value = None + setattr(invoice.logistics, key, value) + else: + logistics_dict = invoice_data.logistics.model_dump() + logistics_dict["invoice_id"] = invoice.id + logistics_dict["tenant_id"] = tenant_id + logistics_dict["company_id"] = company_id + new_logistics = models.InvoiceLogistics(**logistics_dict) + db.add(new_logistics) + db.commit() db.refresh(invoice) return invoice diff --git a/frontend/src/lib/api/dashboard/a76/invoices.ts b/frontend/src/lib/api/dashboard/a76/invoices.ts index b2a855c0..6b00d38b 100644 --- a/frontend/src/lib/api/dashboard/a76/invoices.ts +++ b/frontend/src/lib/api/dashboard/a76/invoices.ts @@ -161,6 +161,12 @@ export interface InvoiceLogistics { is_ctm_process?: string | null; is_subdivision?: boolean | null; acts_as_cd?: boolean | null; + equipment_reviewed?: boolean | null; + pedimento_arrived?: boolean | null; + green_light_mx?: boolean | null; + green_light_us?: boolean | null; + red_light_mx?: boolean | null; + red_light_us?: boolean | null; } export interface InvoiceSalesDetails { diff --git a/frontend/src/lib/components/dashboard/invoices/edit/ManifestSelectorModal.svelte b/frontend/src/lib/components/dashboard/invoices/edit/ManifestSelectorModal.svelte new file mode 100644 index 00000000..daeba6db --- /dev/null +++ b/frontend/src/lib/components/dashboard/invoices/edit/ManifestSelectorModal.svelte @@ -0,0 +1,128 @@ + + + + + + Seleccionar Manifiesto + + Busca y selecciona un manifiesto del catálogo de exportación para vincular a esta factura. + + + +
+
+
+ + e.key === 'Enter' && searchManifests()} + /> +
+ +
+ +
+ + + + + + + + + {#if manifests.length === 0} + + + + {:else} + {#each manifests as manifest} + handleSelect(manifest)} + > + + + + {/each} + {/if} + +
Número de ManifiestoDescripción
+ {#if loading} + Buscando manifiestos... + {:else} + No se encontraron resultados + {/if} +
+ {manifest.manifest_number} + + {manifest.description || '-'} +
+
+
+
+
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/continuation-tab-form.svelte b/frontend/src/lib/components/dashboard/invoices/edit/continuation-tab-form.svelte index efd1ed4f..0df763c5 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/continuation-tab-form.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/continuation-tab-form.svelte @@ -6,37 +6,49 @@ import { Button } from '$lib/components/ui/button'; import type { Invoice } from '$lib/api/dashboard/a76/invoices'; - let { + let { invoice, formData = $bindable(), - exists = $bindable() - }: { + exists = $bindable(), + operationType = undefined + }: { invoice: Invoice | null; formData?: any; exists?: boolean; + operationType?: number; } = $props(); if (!formData && invoice) { - formData = { + formData = { // Campos de esta pestaña - numero_tipo_transporte: '', - es_ferrocarril: 'no', - numero_bl: '', - cantidad_guias_embarque: null, - destino_origen: '', - puerto_entrada: '', + numero_tipo_transporte: + invoice.logistics?.transport_num || invoice.logistics?.vehicle_num || '', + es_ferrocarril: invoice.logistics?.is_rail ? 'si' : 'no', + numero_bl: invoice.logistics?.bill_number || '', + cantidad_guias_embarque: invoice.logistics?.guide_number || null, + destino_origen: invoice.logistics?.destination_location || '', + puerto_entrada: invoice.logistics?.origin_location || '', // Checkboxes - fue_revisado_equipo: false, - sub_division: false, - funge_como_cd: false, - llego_pedimento: false, + fue_revisado_equipo: invoice.logistics?.equipment_reviewed || false, + sub_division: invoice.logistics?.is_subdivision || false, + funge_como_cd: invoice.logistics?.acts_as_cd || false, + llego_pedimento: invoice.logistics?.pedimento_arrived || false, // Errores errores_facturacion: [], // Semáforos - semaforo_verde_aduana_mexicana: false, - semaforo_verde_aduana_americana: false, - semaforo_rojo_aduana_mexicana: false, - semaforo_rojo_aduana_americana: false + semaforo_verde_aduana_mexicana: invoice.logistics?.green_light_mx || false, + semaforo_verde_aduana_americana: invoice.logistics?.green_light_us || false, + semaforo_rojo_aduana_mexicana: invoice.logistics?.red_light_mx || false, + semaforo_rojo_aduana_americana: invoice.logistics?.red_light_us || false, + // New Export fields + is_mixed: invoice.compliance_mx?.is_mixed ? 'si' : 'no', + reason_export: invoice.compliance_mx?.reason_export || '1', + purchase_order: invoice.purchase_order || '', + payment_terms: invoice.payment_terms || '', + handling_fees: invoice.handling_fees || 0, + cfdi_uuid: invoice.cfdi_uuid || '', + path_pdf: invoice.path_pdf || '', + path_xml: invoice.path_xml || '' }; exists = true; } else if (!formData) { @@ -59,7 +71,16 @@ semaforo_verde_aduana_mexicana: false, semaforo_verde_aduana_americana: false, semaforo_rojo_aduana_mexicana: false, - semaforo_rojo_aduana_americana: false + semaforo_rojo_aduana_americana: false, + // New Export fields + is_mixed: 'no', + reason_export: '1', + purchase_order: '', + payment_terms: '', + handling_fees: 0, + cfdi_uuid: '', + path_pdf: '', + path_xml: '' }; exists = false; } @@ -68,17 +89,21 @@
-
-

Información General

+
+

Información General

- +
-
+
@@ -98,48 +123,127 @@
- - + +
- -
- - + +
+
+ + +
+ + {#if operationType === 1} +
+ + +
+ + +
+
+ + +
+
+
+ {/if}
-
- - -
+ {#if operationType !== 1} +
+ + +
+ {/if} + + {#if operationType === 1} +
+
+ + +
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ +
+ + USD +
+
+
+ {/if}
-
- - -
-
- - -
-
- - -
-
- - -
-
+ {#if operationType !== 1} +
+ + +
+
+ + +
+
+ + +
+ {/if} + {#if operationType !== 1} +
+ + +
+ {/if} +
-
+

Errores de Facturación

- -
+ +
@@ -167,38 +271,59 @@
- +
-
+
-
+
-
+
- +
-
+
- +
-
+
- +
-
+
- +
+ + {#if operationType === 1} + +
+

+ DATOS CFDI +

+
+ + +
+
+ + +
+
+ + +
+
+ {/if}
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/general-tab-form.svelte b/frontend/src/lib/components/dashboard/invoices/edit/general-tab-form.svelte index 0b8ff489..c79b530c 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/general-tab-form.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/general-tab-form.svelte @@ -3,6 +3,9 @@ import { Label } from '$lib/components/ui/label'; import * as Select from '$lib/components/ui/select'; import * as RadioGroup from '$lib/components/ui/radio-group'; + import { Button } from '$lib/components/ui/button'; + import { Search } from 'lucide-svelte'; + import ManifestSelectorModal from './ManifestSelectorModal.svelte'; import type { Invoice } from '$lib/api/dashboard/a76/invoices'; import type { InvoiceType } from '$lib/api/dashboard/reference_data/invoice_types'; import type { CustomsBroker } from '$lib/api/dashboard/a76/customs-brokers'; @@ -51,6 +54,12 @@ exchangeRate?: number | null; } = $props(); + let showManifestModal = $state(false); + + function handleManifestSelect(manifest: any) { + formData.manifest_number = manifest.manifest_number; + } + // Sync exchangeRate prop to formData $effect(() => { if (exchangeRate !== undefined && formData) { @@ -683,15 +692,23 @@ {/if} {#if operationType === 1}
- - + +
+ + +
{/if}
@@ -923,3 +940,5 @@
+ + diff --git a/frontend/src/lib/components/dashboard/invoices/edit/invoice-top-fields.svelte b/frontend/src/lib/components/dashboard/invoices/edit/invoice-top-fields.svelte index e1739478..cbbad85e 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/invoice-top-fields.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/invoice-top-fields.svelte @@ -1,196 +1,214 @@ -
-
- - { - formData.operation_type = v; - }} - > - - - {formData.operation_type - ? (formData.operation_type === 'exp' ? 'Exp' : 'Imp') - : '...'} - - - - Exportación - Importación - - -
-
- - { - formData.invoice_type = v ?? ''; - }} - > - - - {formData.invoice_type - ? `${formData.invoice_type}` - : '...'} - - - - {#each invoiceTypes as type} - - {type.key} - {type.description} - - {/each} - - -
+
+
+ + { + formData.operation_type = v; + }} + > + + + {formData.operation_type ? (formData.operation_type === 'exp' ? 'Exp' : 'Imp') : '...'} + + + + Exportación + Importación + + +
+
+ + { + formData.invoice_type = v ?? ''; + }} + > + + + {formData.invoice_type ? `${formData.invoice_type}` : '...'} + + + + {#each invoiceTypes as type} + + {type.key} - {type.description} + + {/each} + + +
-
- - { - formData.is_pedimento_pending = checked; - }} - /> -
-
- - { - formData.pedimento_id = v ? parseInt(v) : null; - if (v) { - handlePedimentoChange(v); - } - }} - > - - - {formData.pedimento || 'Selecciona pedimento...'} - - - - {#each pedimentos as pedimento} - - {pedimento.customs_office?.slice(0, 2)}-{pedimento.license}-{pedimento.pedimento_number} - - {/each} - - -
+
+ + { + formData.is_pedimento_pending = checked; + }} + /> +
+
+ + { + formData.pedimento_id = v ? parseInt(v) : null; + if (v) { + handlePedimentoChange(v); + } + }} + > + + + {formData.pedimento || 'Selecciona pedimento...'} + + + + {#each pedimentos as pedimento} + + {pedimento.customs_office?.slice(0, 2)}-{pedimento.license}-{pedimento.pedimento_number} + + {/each} + + +
-
- - -
+
+ + +
-
- - -
+
+ + +
-
- - -
+
+ + +
-
- - -
+
+ + +
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/observations-tab-form.svelte b/frontend/src/lib/components/dashboard/invoices/edit/observations-tab-form.svelte index 722afc8b..7d69bef7 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/observations-tab-form.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/observations-tab-form.svelte @@ -45,7 +45,7 @@ alternate_invoice: invoice.alternate_invoice || '', valuation_method: invoice.compliance_mx?.value_method || null, // New fields for Export - other_deductibles: null, // Need to confirm where this maps + other_deductibles: invoice.financials?.other_deductibles || null, proforma_number: invoice.proforma_number || '', sub_division: invoice.compliance_mx?.subdivision || '', acts_as_cd: invoice.logistics?.acts_as_cd || false, diff --git a/frontend/src/lib/components/dashboard/invoices/edit/others-tab-form.svelte b/frontend/src/lib/components/dashboard/invoices/edit/others-tab-form.svelte index c08d1e4f..4a153cd8 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/others-tab-form.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/others-tab-form.svelte @@ -10,16 +10,18 @@ import { Plus, Upload } from 'lucide-svelte'; import type { Invoice } from '$lib/api/dashboard/a76/invoices'; - let { + let { invoice, formData = $bindable(), exists = $bindable(), - transportModes = [] - }: { + transportModes = [], + operationType = undefined + }: { invoice: Invoice | null; formData?: any; exists?: boolean; transportModes?: any[]; + operationType?: number; } = $props(); if (!formData && invoice) { @@ -32,15 +34,25 @@ print_stamp: invoice.financials?.seal_value_2500 || false, rule_3121_parties_ii: false, related_doc_id: invoice.related_doc_id || null, - code_signature: invoice.compliance_mx?.code_signature || '', + code_signature: invoice.compliance_mx?.signature_key || '', electronic_signature: invoice.compliance_mx?.electronic_signature || '', - mandatory_person: '', + mandatory_person: invoice.compliance_mx?.acts_as || '', contingency_mode: invoice.compliance_mx?.contingency_mode || false, - cove: invoice.compliance_mx?.origin_destination_cove || '', + cove: invoice.compliance_mx?.edocument || '', operation_num: invoice.compliance_mx?.vucem_operation_num || '', adendas: invoice.compliance_mx?.addendum_vu || '', observations_vu: invoice.vu_observations || '', - certified_number: invoice.compliance_mx?.certificate_number || '', + certified_number: invoice.compliance_mx?.certificate_number || '', + // New Export fields + bill_number: invoice.logistics?.bill_number || '', + guide_number: invoice.logistics?.guide_number || '', + shipment_number: invoice.logistics?.shipment_number || '', + option_iv18: invoice.option_iv18 || '', + delivered_status: invoice.logistics?.delivered_status || false, + received_by: invoice.logistics?.received_by || '', + delivery_date: invoice.logistics?.delivery_date + ? invoice.logistics.delivery_date.split('T')[0] + : '' }; exists = true; } else if (!formData) { @@ -61,7 +73,15 @@ operation_num: '', adendas: '', observations_vu: '', - certified_number: '', + certified_number: '', + // New Export fields + bill_number: '', + guide_number: '', + shipment_number: '', + option_iv18: '', + delivered_status: false, + received_by: '', + delivery_date: '' }; exists = false; } @@ -70,7 +90,6 @@ let rfc = $state(''); let curp = $state(''); - function loadInfo() { // Función para cargar información console.log('Cargar información'); @@ -78,18 +97,18 @@
-
- +
- formData.transport_mode = value || 'TRUCK'} + (formData.transport_mode = value || 'TRUCK')} > - {transportModes.find(m => m.key === formData.transport_mode)?.name || 'Seleccionar modo'} + {transportModes.find((m) => m.key === formData.transport_mode)?.name || + 'Seleccionar modo'} {#each transportModes as mode} @@ -101,42 +120,106 @@
- -
- -
- - + {#if operationType !== 1} + +
+ +
+ + +
-
- -
- - - formData.is_mixed = v === 'yes'} - class="flex gap-4" - > -
- - -
-
- - -
-
- -
+ +
+ + + (formData.is_mixed = v === 'yes')} + class="flex gap-4" + > +
+ + +
+
+ + +
+
+ +
+ {:else} + +
+ + +
+
+ + +
+
+ + +
+
+ + (formData.option_iv18 = v || '')} + > + + {formData.option_iv18 || 'Seleccionar opción'} + + + Opción 1 + + + +
-
- - -
+ +
+

+ Datos Entrega +

+
+ + +
+
+ + +
+
+ + +
+
+ {/if} + + {#if operationType !== 1} +
+ + +
+ {/if}
@@ -145,22 +228,18 @@