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..90386af6 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,9 +109,11 @@ 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 pedimento sea de importación (hardcoded restriction) + if pedimento.operation_type != "imp": errors.add_error( field="compliance_mx.pedimento_id", message="El Pedimento seleccionado no corresponde a una Importación.", @@ -120,7 +122,10 @@ def validate_common( value=pedimento.operation_type, ) else: - if pedimento.regime in ["EXD", "ETE", "ETR"]: + # Validar regímenes incompatibles + export_only_regimes = ["EXD", "ETE", "ETR"] + + if 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 +133,10 @@ def validate_common( code="INVALID_REGIME", value=pedimento.regime, ) - else: + + # 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 +148,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 +214,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 +251,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 +379,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..79a6f0cb 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 @@ -14,7 +14,7 @@ def validate_create(db: Session, invoice: InvoiceHeaderCreate, tenant_id: int, c if not invoice.invoice_type: errors.add_required_error("invoice_type") - if not invoice.document_type: + if not invoice.document_type and invoice.invoice_type != "MEX": errors.add_required_error("document_type") if not invoice.invoice_number: @@ -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/imports/temporary/validators/update.py b/backend/api/v1/modules/a76/invoices/imports/temporary/validators/update.py index da4cc970..97ee1843 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 @@ -224,9 +224,10 @@ def validate_update( else: invoice_data.compliance_mx.aduana = existing_invoice.compliance_mx.aduana if existing_invoice.compliance_mx else None - # Validar que aduana sea obligatorio - if not invoice_data.compliance_mx or not invoice_data.compliance_mx.aduana: - errors.add_required_error("aduana") + # Validar que aduana sea obligatorio (excepto para MEX) + if existing_invoice.invoice_type != "MEX": + if not invoice_data.compliance_mx or not invoice_data.compliance_mx.aduana: + errors.add_required_error("aduana") # Columna AC: Sección de Despacho / Puerto de Entrada (Opcional) if invoice_data.compliance_mx and invoice_data.compliance_mx.port_of_entry: diff --git a/backend/api/v1/modules/a76/invoices/models.py b/backend/api/v1/modules/a76/invoices/models.py index 8e6f35d5..f3f9e0c7 100644 --- a/backend/api/v1/modules/a76/invoices/models.py +++ b/backend/api/v1/modules/a76/invoices/models.py @@ -58,7 +58,7 @@ class TransportType(str, Enum): PLATES = "licence plates" TRUCK = "truck" VESSEL = "vessel" - BARGE = "rail barge" + BARGE = "rail_barge" CONTAINER = "container" AIRPLANE = "airplane" GONDOLA = "gondola" @@ -82,9 +82,10 @@ class InvoiceHeader(Base, TenantScopedMixin, TimestampMixin): invoice_type: Mapped[str] = mapped_column( ForeignKey("public.invoice_types.key") ) # TIPOFACTURA / TIPODOC - document_type: Mapped[str] = mapped_column( - ForeignKey("public.pedimento_regimens.code") - ) # CLAVEDOCUMENTO / Clave de documento + document_type: Mapped[Optional[str]] = mapped_column( + ForeignKey("public.pedimento_regimens.code"), nullable=True + ) + # CLAVEDOCUMENTO / Clave de documento invoice_number: Mapped[str] = mapped_column( String(100) ) # FACTURAIMPO/FACTURAEXPO/FACTURAREMISION/FACTURAENVIO/FACTURASALIDA @@ -299,6 +300,9 @@ class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin): act_value: Mapped[Optional[str]] = mapped_column( String(5) ) # ACTVALOR / Actualizar valor + rule_3121_parties_ii: Mapped[Optional[bool]] = mapped_column( + Boolean, default=False, server_default="false" + ) # REGLA3121PARTESII / Regla 3.1.21 Partes II is_pedimento_pending: Mapped[Optional[bool]] = mapped_column( Boolean, default=False, server_default="false" ) # PED_PENDIENTE_ASIGNAR (Mapear 1 -> True, 0 -> False) @@ -375,7 +379,7 @@ class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin): String(1) ) # RAZONEXPORTACION / Razón de exportación signature_key: Mapped[Optional[str]] = mapped_column( - String(10) + String(100) ) # CLAVEFIRMA / Clave de firma # SM specific @@ -488,6 +492,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..1ac300c9 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, @@ -22,8 +22,8 @@ class InvoiceHeaderBase(BaseModel): invoice_type: Optional[str] = Field( None, max_length=5, description="Invoice type key" ) - document_type: str = Field( - ..., max_length=3, description="Document type (Regimen Aduanero)" + document_type: Optional[str] = Field( + None, max_length=3, description="Document type (Regimen Aduanero)" ) invoice_number: Optional[str] = Field( None, max_length=100, description="Invoice number" @@ -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") @@ -139,6 +139,9 @@ class InvoiceComplianceMxBase(BaseModel): which_exchange_rate: Optional[str] = Field( None, max_length=5, description="Which exchange rate" ) + rule_3121_parties_ii: Optional[bool] = Field( + False, description="Is Rule 3.1.21 Parties II" + ) value_method: Optional[str] = Field(None, max_length=2, description="Value method") act_value: Optional[str] = Field(None, max_length=5, description="Act value") is_pedimento_pending: bool = Field(..., description="Is pedimento pending") @@ -187,7 +190,7 @@ class InvoiceComplianceMxBase(BaseModel): None, max_length=1, description="Reason for export" ) signature_key: Optional[str] = Field( - None, max_length=10, description="Signature key" + None, max_length=100, description="Signature key" ) sem_id: Optional[int] = Field(None, description="SEM ID") @@ -248,6 +251,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 +322,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..f3b0681b 100644 --- a/backend/api/v1/modules/a76/invoices/services.py +++ b/backend/api/v1/modules/a76/invoices/services.py @@ -128,6 +128,10 @@ class InvoiceService: invoice_dict = clean_dict(raw_invoice_dict) invoice_dict["tenant_id"] = tenant_id invoice_dict["company_id"] = company_id + + # Ensure document_type respects DB constraints for MEX invoices (bypass clean_dict) + if invoice_dict.get("invoice_type") == "MEX" and not invoice_dict.get("document_type"): + invoice_dict["document_type"] = None new_invoice = models.InvoiceHeader(**invoice_dict) @@ -301,6 +305,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/backend/api/v1/modules/public/reference_data/invoice_types/seed.py b/backend/api/v1/modules/public/reference_data/invoice_types/seed.py index 151163dd..1b42742a 100644 --- a/backend/api/v1/modules/public/reference_data/invoice_types/seed.py +++ b/backend/api/v1/modules/public/reference_data/invoice_types/seed.py @@ -28,6 +28,13 @@ seed = [ "both", "imp", ), + ( + "REP", + "REPARACION", + "REPARACION DE ACTIVOS FIJOS", + "fixed asset", + "imp", + ), # === TIPOS DE EXPORTACION === ("DONAC", "DONACION", "", "both", "exp"), diff --git a/frontend/messages/en.json b/frontend/messages/en.json index e2563ba5..8abbec08 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -82,7 +82,8 @@ "temporary": "Temporary", "definitive": "Definitive", "mexican_purchases": "Mexican Purchases", - "regime_change": "Regime Change" + "regime_change": "Regime Change", + "repair": "Repair" }, "export_invoices": { "title": "Export Invoices", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 1caa7503..3be2f6be 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -82,7 +82,8 @@ "temporary": "Temporal", "definitive": "Definitiva", "mexican_purchases": "Compras mexicanas", - "regime_change": "Cambio de régimen" + "regime_change": "Cambio de régimen", + "repair": "Reparación" }, "export_invoices": { "title": "Facturas de exportación", diff --git a/frontend/src/lib/api/dashboard/a76/invoices.ts b/frontend/src/lib/api/dashboard/a76/invoices.ts index 91cc735e..99973e13 100644 --- a/frontend/src/lib/api/dashboard/a76/invoices.ts +++ b/frontend/src/lib/api/dashboard/a76/invoices.ts @@ -73,6 +73,7 @@ export interface InvoiceComplianceMx { reason_export?: string | null; signature_key?: string | null; sem_id?: number | null; + rule_3121_parties_ii?: boolean | null; } export interface InvoiceFinancials { @@ -103,6 +104,7 @@ export interface InvoiceFinancials { insurance_value?: number | null; packaging?: number | null; other_increments?: number | null; + other_deductibles?: number | null; total_increments_mn?: number | null; total_increments_me?: number | null; iva_mn?: number | null; @@ -158,6 +160,14 @@ export interface InvoiceLogistics { payment_date?: string | null; payment_receipt_num?: string | null; 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/export/manifest/manifest-form.svelte b/frontend/src/lib/components/dashboard/export/manifest/manifest-form.svelte index 3bb3f9b4..9a57d055 100644 --- a/frontend/src/lib/components/dashboard/export/manifest/manifest-form.svelte +++ b/frontend/src/lib/components/dashboard/export/manifest/manifest-form.svelte @@ -269,7 +269,7 @@ // Helper to check if Repar request const isRepar = (t: string | undefined) => { - return t && t.toUpperCase().includes('REPAR'); + return t === 'REPAR'; }; // Categorize 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/PortSelectorModal.svelte b/frontend/src/lib/components/dashboard/invoices/edit/PortSelectorModal.svelte new file mode 100644 index 00000000..73308ed6 --- /dev/null +++ b/frontend/src/lib/components/dashboard/invoices/edit/PortSelectorModal.svelte @@ -0,0 +1,108 @@ + + + + + + Seleccionar Puerto (Aduana/Sección) + Busca y selecciona una sección aduanera de la lista. + + +
+
+ + +
+ +
+ {#if loading} +
+ +
+ {:else if filteredSections.length > 0} + + + + + + + + + + {#each filteredSections as section} + handleSelect(section)}> + + + + + {/each} + +
CódigoNombre / Sección
{section.customs_code}{section.section_name} + +
+ {:else} +
No se encontraron resultados
+ {/if} +
+
+ + + + +
+
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..1f89d8fc 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 @@ -4,39 +4,56 @@ import { Checkbox } from '$lib/components/ui/checkbox'; import { RadioGroup, RadioGroupItem } from '$lib/components/ui/radio-group'; import { Button } from '$lib/components/ui/button'; + import { Search } from 'lucide-svelte'; + import PortSelectorModal from './PortSelectorModal.svelte'; import type { Invoice } from '$lib/api/dashboard/a76/invoices'; - let { + let { invoice, formData = $bindable(), - exists = $bindable() - }: { + exists = $bindable(), + operationType = undefined, + invoiceType = undefined + }: { invoice: Invoice | null; formData?: any; exists?: boolean; + operationType?: number; + invoiceType?: string; } = $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 || '', + vehicle_data: invoice.logistics?.vehicle_data || '', // 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) { @@ -48,6 +65,7 @@ cantidad_guias_embarque: null, destino_origen: '', puerto_entrada: '', + vehicle_data: '', // Checkboxes fue_revisado_equipo: false, sub_division: false, @@ -59,27 +77,46 @@ 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; } + let showPortModal = $state(false); + + function handlePortSelect(section: any) { + formData.puerto_entrada = section.customs_code; + }
-
-

Información General

+
+

Información General

- +
-
- +
+ +
@@ -98,48 +135,141 @@
- - + +
- -
- - + +
+
+ + +
+ + {#if operationType === 1 || invoiceType === 'CR' || invoiceType === 'REP'} +
+ + +
+ + +
+
+ + +
+
+
+ {/if}
-
- - -
+ {#if operationType !== 1 && invoiceType !== 'CR'} +
+ +
+ + +
+
+ {/if} + + {#if operationType === 1 || invoiceType === 'CR'} +
+
+ + +
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ +
+ + USD +
+
+
+ {/if}
-
- - -
-
- - -
-
- - -
-
- - -
-
+ {#if operationType !== 1 && invoiceType !== 'CR'} +
+ + +
+ {#if invoiceType !== 'REP' && invoiceType !== 'REPAR'} +
+ + +
+
+ + +
+
+ + +
+ {/if} + {/if} +
-
+

Errores de Facturación

- -
+ +
@@ -167,38 +297,61 @@
- +
-
+
-
+
-
+
- +
-
+
- +
-
+
- +
-
+
- +
+ + {#if operationType === 1 || invoiceType === 'CR'} + +
+

+ 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 e7c11420..dde6f5d1 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, Upload } 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'; @@ -29,8 +32,8 @@ codePedimentoRegimens = [], operationType = undefined, defaultOperationType = undefined, - exchangeRate = undefined - }: { + exchangeRate = undefined + }: { invoice: Invoice | null; formData?: any; invoiceTypes?: InvoiceType[]; @@ -49,8 +52,15 @@ defaultInvoiceType?: string | null; operationType?: number | null; exchangeRate?: number | null; + invoiceType?: string; } = $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) { @@ -69,6 +79,8 @@ sold_to_id: invoice.compliance_mx?.sold_to_id || null, shipped_to_header: invoice.compliance_mx?.shipped_to_header || 'enviado_a', shipped_to_id: invoice.compliance_mx?.shipped_to_id || null, + shipped_by_header: invoice.compliance_mx?.shipped_by_header || 'enviado_por', + shipped_by_id: invoice.compliance_mx?.shipped_by_id || null, customs_broker_id: invoice.compliance_mx?.customs_broker_id || null, customs_broker_us_id: invoice.compliance_mx?.customs_broker_us_id || null, @@ -84,7 +96,10 @@ transport_type: invoice.logistics?.transport_type || '', transport_num: invoice.logistics?.vehicle_num || '', aduana: invoice.compliance_mx?.aduana || '', - document_type: invoice.document_type || '' + document_type: invoice.document_type || '', + manifest_number: invoice.compliance_mx?.manifest_number || '', + code_signature: invoice.compliance_mx?.signature_key || '', + electronic_signature: invoice.compliance_mx?.electronic_signature || '' }; } else { // Creando una nueva factura @@ -96,6 +111,8 @@ sold_to_id: null, shipped_to_header: 'enviado_a', shipped_to_id: null, + shipped_by_header: 'enviado_por', + shipped_by_id: null, customs_broker_id: null, customs_broker_us_id: null, @@ -111,7 +128,10 @@ transport_type: '', transport_num: '', aduana: '', - document_type: '' + document_type: '', + manifest_number: '', + code_signature: '', + electronic_signature: '' }; } } else { @@ -128,6 +148,18 @@ if (!formData.shipped_to_header) { formData.shipped_to_header = 'enviado_a'; } + if (!formData.shipped_by_header) { + formData.shipped_by_header = 'enviado_por'; + } + if (formData.manifest_number === undefined) { + formData.manifest_number = ''; + } + if (formData.code_signature === undefined) { + formData.code_signature = ''; + } + if (formData.electronic_signature === undefined) { + formData.electronic_signature = ''; + } } // Opciones de tipo de peso @@ -160,25 +192,16 @@ const soldToHeaderOptions = $derived([ { value: 'consignado_a', label: 'Consignado a' }, { value: 'vendido_a', label: 'Vendido a' }, - { - value: operationType === 1 ? 'exportado_a' : 'importador', - label: operationType === 1 ? 'Exportado a' : 'Importador' - } + { value: operationType === 1 ? 'exportado_a' : 'importador', label: operationType === 1 ? 'Exportado a' : 'Importador' } ]); const shippedToHeaderOptions = $derived( - operationType === 1 + operationType === 1 || invoiceType === 'CR' ? [ - { value: 'enviado_por', label: 'Enviado Por' }, - { value: 'destinatario', label: 'Destinatario' }, - { value: 'vendido_por', label: 'Vendido Por' }, - { value: 'consignado_a', label: 'Consignado a' }, - { value: 'vendido_a', label: 'Vendido a' }, - { value: 'exportado_a', label: 'Exportado a' }, { value: 'enviado_a', label: 'Enviado a' }, { value: 'transferido_a', label: 'Transferido a' }, { value: 'donado_a', label: 'Donado a' }, - { value: 'notificar_a', label: 'Notificar a' } + { value: 'importador', label: 'Importador' } ] : [ { value: 'enviado_a', label: 'Enviado a' }, @@ -186,6 +209,26 @@ ] ); + const shippedByHeaderOptions = $derived( + operationType === 1 || invoiceType === 'CR' + ? [ + { value: 'enviado_por', label: 'Enviado Por' }, + { value: 'destinatario', label: 'Destinatario' }, + { value: 'vendido_por', label: 'Vendido Por' }, + { value: 'consignado_a', label: 'Consignado a' }, + { value: 'vendido_a', label: 'Vendido a' }, + { value: 'exportado_a', label: 'Exportado a' }, + { value: 'enviado_a', label: 'Enviado a' }, + { value: 'transferido_a', label: 'Transferido a' }, + { value: 'donado_a', label: 'Donado a' }, + { value: 'notificar_a', label: 'Notificar a' } + ] + : [ + { value: 'enviado_a', label: 'Enviado a' }, + { value: 'transferido_a', label: 'Transferido a' } + ] + ); + // Combinar clientes y proveedores para shipped_to, evitando duplicados de tipo "both" const allClientsProviders = $derived.by(() => { const uniqueMap = new Map(); @@ -258,7 +301,7 @@
-
+ {#if invoiceType !== 'MEX'}

Datos del pedimento

@@ -278,7 +321,7 @@

{formData.regimen_pedimento || '-'}

-
+ {/if}

Clientes - Proveedores - Agente Aduanal @@ -383,85 +426,80 @@ *

-
- { - formData.shipped_to_header = v ?? ''; - }} - > - - - {shippedToHeaderOptions.find( - (o) => o.value === (formData.shipped_to_header || shippedToHeaderOptions[0]?.value) - )?.label || 'Selecciona encabezado...'} - - - - {#each shippedToHeaderOptions as option} - - {option.label} - - {/each} - - - { - formData.shipped_to_id = v ? parseInt(v) : null; - }} - > - - - {#if formData.shipped_to_id} - {allClientsProviders.find((cp) => cp.id === formData.shipped_to_id)?.name || - 'Selecciona...'} - {:else} - Selecciona... - {/if} - - - - {#each allClientsProviders as cp} - - {cp.name} - - {/each} - - - * -
+
+ { + formData.shipped_to_header = v ?? ''; + }} + > + + + {shippedToHeaderOptions.find(o => o.value === (formData.shipped_to_header || shippedToHeaderOptions[0]?.value))?.label || 'Selecciona encabezado...'} + + + + {#each shippedToHeaderOptions as option} + + {option.label} + + {/each} + + + { + formData.shipped_to_id = v ? parseInt(v) : null; + }} + > + + + {#if formData.shipped_to_id} + {allClientsProviders.find(cp => cp.id === formData.shipped_to_id)?.name || 'Selecciona...'} + {:else} + Selecciona... + {/if} + + + + {#each allClientsProviders as cp} + + {cp.name} + + {/each} + + + * +
-
- - { - formData.customs_broker_id = v ? parseInt(v) : null; - }} - > - - - {formData.customs_broker_id - ? customsBrokers.find((cb) => cb.id === formData.customs_broker_id)?.name || - 'Selecciona...' - : 'Selecciona...'} - - - - {#each customsBrokers as broker} - - {broker.name} - - {/each} - - -
+ +
+ + { + formData.customs_broker_id = v ? parseInt(v) : null; + }} + > + + + {formData.customs_broker_id + ? customsBrokers.find(cb => cb.id === formData.customs_broker_id)?.name || 'Selecciona...' + : 'Selecciona...'} + + + + {#each customsBrokers as broker} + + {broker.name} + + {/each} + + +
@@ -491,150 +529,162 @@
- -
- -
-
-

- Tipo de Moneda - Pesos Netos y Brutos -

-

- Tipo de cambio: - - {exchangeRate !== undefined && exchangeRate !== null && exchangeRate !== 0 - ? Number(exchangeRate).toFixed(4) - : formData.exchange_rate && formData.exchange_rate !== 0 - ? Number(formData.exchange_rate).toFixed(4) - : 'N/A'} - -

-
+ +
+ +
+
+

Tipo de Moneda - Pesos Netos y Brutos

+

+ Tipo de cambio: + + {(exchangeRate !== undefined && exchangeRate !== null) + ? (exchangeRate === 0 ? 'N/A' : Number(exchangeRate).toFixed(4)) + : (formData.exchange_rate ? Number(formData.exchange_rate).toFixed(4) : 'N/A')} + +

+
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+
+ {#if formData.currency === 'manual'} +
+ + { + formData.currency_type = v ?? ''; + }} + > + + + {formData.currency_type || '...'} + + + + {#each currencyTypes as currencyType} + + {currencyType.code} + + {/each} + + +
+ {/if} +
+
+ + { + formData.weight_type = v ?? 'kgs'; + }} + > + + + {weightTypeOptions.find(w => w.value === formData.weight_type)?.label || 'Kilogramos (kg)'} + + + + {#each weightTypeOptions as weightType} + + {weightType.label} + + {/each} + + +
- -
- -
- - + {#if operationType !== 1 && invoiceType !== 'MEX' && invoiceType !== 'CR' && invoiceType !== 'REP' && invoiceType !== 'REPAR'} +
+ +
-
- - + {/if} + {#if operationType === 1 || invoiceType === 'CR'} +
+ +
+ + +
-
- - -
- -
- {#if formData.currency === 'manual'} -
- - { - formData.currency_type = v ?? ''; - }} - > - - - {formData.currency_type || '...'} - - - - {#each currencyTypes as currencyType} - - {currencyType.code} - - {/each} - - -
- {/if} -
-
- - { - formData.weight_type = v ?? 'kgs'; - }} - > - - - {weightTypeOptions.find((w) => w.value === formData.weight_type)?.label || - 'Kilogramos (kg)'} - - - - {#each weightTypeOptions as weightType} - - {weightType.label} - - {/each} - - -
+ {/if} +
+
-
- - -
-
-
- - -
-

Transportista

- -
-
- - { - formData.carrier_id = v || null; - }} - > - - - {#if formData.carrier_id} - {transporters.find( - (t) => String(t.transporter_key) === String(formData.carrier_id) - )?.name || formData.carrier_id} - {:else if transporters.length > 0} - Selecciona transportista... - {:else} - Sin datos - {/if} - - - - {#each transporters as transporter} - - {transporter.transporter_key} - - {/each} - - -
+ +
+

Transportista

+ +
+ {#if invoiceType !== 'MEX'} +
+ + { + formData.carrier_id = v || null; + }} + > + + + {#if formData.carrier_id} + {transporters.find(t => String(t.transporter_key) === String(formData.carrier_id))?.name || formData.carrier_id} + {:else if transporters.length > 0} + Selecciona transportista... + {:else} + Sin datos + {/if} + + + + {#each transporters as transporter} + + {transporter.transporter_key} + + {/each} + + +
+ {/if}
@@ -757,71 +807,91 @@
-
- - { - formData.aduana = v ?? ''; - }} - > - - - {#if formData.aduana} - {customsSections.find((cs) => cs.customs_code === formData.aduana)?.section_name || - formData.aduana} - {:else if customsSections.length > 0} - Selecciona aduana... - {:else} - Sin datos - {/if} - - - - {#each customsSections as section} - - {section.customs_code} - {section.section_name} - - {/each} - - -
+ {#if invoiceType !== 'MEX'} +
+ + { + formData.aduana = v ?? ''; + }} + > + + + {#if formData.aduana} + {customsSections.find(cs => cs.customs_code === formData.aduana)?.section_name || formData.aduana} + {:else if customsSections.length > 0} + Selecciona aduana... + {:else} + Sin datos + {/if} + + + + {#each customsSections as section} + + {section.customs_code} - {section.section_name} + + {/each} + + +
+ {/if} -
- - { - formData.document_type = v ?? ''; - }} - > - - - {#if formData.document_type} - {codePedimentoRegimens.find((r) => r.regimen_code === formData.document_type) - ?.regimen_code || formData.document_type} - {:else if filteredRegimens.length > 0} - Selecciona régimen... - {:else if operationType} - Sin regímenes para tipo {operationType} - {:else} - Selecciona tipo de operación primero - {/if} - - - - {#each filteredRegimens as regimen} - - {regimen.regimen_code} - - {/each} - - -
-
-
+ {#if invoiceType !== 'MEX'} +
+ + { + formData.document_type = v ?? ''; + }} + > + + + {#if formData.document_type} + {codePedimentoRegimens.find(r => r.regimen_code === formData.document_type)?.regimen_code || formData.document_type} + {:else if filteredRegimens.length > 0} + Selecciona régimen... + {:else if operationType} + Sin regímenes para tipo {operationType} + {:else} + Selecciona tipo de operación primero + {/if} + + + + {#each filteredRegimens as regimen} + + {regimen.regimen_code} + + {/each} + + +
+ {/if} + + {#if invoiceType === 'MEX'} +
+ +
+ + +
+
+ {/if} +
+
+ + 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 cce13893..82adf098 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 @@ -13,7 +13,8 @@ invoiceTypes = [], pedimentos = [], defaultOperationType = undefined, - defaultInvoiceType = undefined + defaultInvoiceType = undefined, + invoiceType = undefined }: { invoice: Invoice | null; formData?: any; @@ -21,6 +22,7 @@ pedimentos?: Pedimento[]; defaultOperationType?: string | null; defaultInvoiceType?: string | null; + invoiceType?: string; } = $props(); function handlePedimentoChange(pedimentoId: string) { @@ -73,16 +75,18 @@ pedimento_id: invoice?.compliance_mx?.pedimento_id || '', remesa: invoice?.compliance_mx?.remesa || '', invoice_number: invoice?.invoice_number || '', - invoice_date: - invoice?.invoice_date || (invoice ? '' : new Date().toISOString().split('T')[0]), - emission_date: invoice?.emission_date || new Date().toISOString().split('T')[0], + invoice_date: invoice?.invoice_date || new Date().toISOString().split('T')[0], + emission_date: new Date().toISOString().split('T')[0], operation_type: operationType, invoice_type: invoice?.invoice_type || (defaultInvoiceType ?? ''), // Campos del pedimento (se llenarán al seleccionar un pedimento) fecha_pedimento_del: '', fecha_pedimento_al: '', clave_pedimento: '', - regimen_pedimento: '' + regimen_pedimento: '', + // Campos específicos para MEX + iva_factor: invoice?.financials?.iva_factor || '', + alternate_invoice: invoice?.alternate_invoice || '' }; } else { // Si formData ya existe pero operation_type está vacío, usar defaultOperationType @@ -93,7 +97,29 @@ ) { formData.operation_type = defaultOperationType; } + // Ensure new fields exist if formData was created before + if (formData.iva_factor === undefined) + formData.iva_factor = invoice?.financials?.iva_factor || ''; + if (formData.alternate_invoice === undefined) + formData.alternate_invoice = invoice?.alternate_invoice || ''; } + // Filter invoice types based on operation type + let filteredInvoiceTypes = $derived( + invoiceTypes.filter((type) => { + if (!formData.operation_type) return true; + return type.operation === 'both' || type.operation === formData.operation_type; + }) + ); + + // Reset invoice_type if not valid for new operation_type + $effect(() => { + if (formData.operation_type && formData.invoice_type) { + const isValid = filteredInvoiceTypes.some((t) => t.key === formData.invoice_type); + if (!isValid) { + formData.invoice_type = ''; + } + } + }); @@ -137,7 +163,7 @@ - {#each invoiceTypes as type} + {#each filteredInvoiceTypes as type} {type.key} - {type.description} @@ -146,47 +172,52 @@
-
- - { - 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} - - -
+ {#if invoiceType !== 'MEX'} +
+ + { + 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} + + +
-
- - -
+
+ + +
+ {/if}
+ + {#if invoiceType === 'MEX'} +
+ + +
+
+ + +
+ {/if}
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 785a6bb3..cce7734a 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 @@ -15,11 +15,15 @@ let { invoice, formData = $bindable(), - exists = $bindable() + exists = $bindable(), + operationType = undefined, + invoiceType = undefined }: { invoice: Invoice | null; formData?: any; exists?: boolean; + operationType?: number; + invoiceType?: string; } = $props(); let imported = 0; @@ -49,6 +53,7 @@ let isLoadingMore = $state(false); let isLoadingItems = $state(false); let isSaving = $state(false); + let focusedLine = $state(null); // Sheet states let showItemSheet = $state(false); @@ -109,6 +114,10 @@ isLoadingMore = false; } + function handleRowClick(line: any) { + focusedLine = line; + } + function handleScroll(e: Event) { const target = e.target as HTMLDivElement; const threshold = 100; @@ -251,10 +260,10 @@ } // Load part number data - if (line.part_number_id) { + if (line.part_number) { try { const response = await fetch( - `/api-sveltekit/parts/${line.part_number_id}?company_id=${activeCompanyId}`, + `/api-sveltekit/parts/${line.part_number}?company_id=${activeCompanyId}`, { method: 'GET', headers: { 'Content-Type': 'application/json' } } ); if (response.ok) { @@ -698,11 +707,11 @@
-
-
+
+

Items de la Factura

@@ -710,49 +719,147 @@
- Línea - P/S - Clase - Descripcion Clase - Cant. Importada - U.M. - Preferencia - Contiene Subpartida - Partida Principal - Acciones + {#if operationType === 1} + Línea + Factura Impo + Lin Impo + P/S + Cant. Exportada + Clase + Num Parte + Descripción Esp. + Subpartidas + Partida Princ. + {:else if invoiceType === 'CR'} + Línea + Factura Impo + Lin Impo + P/S + Cantidad Exportada + Clase + Número de Parte + Descripción en Español + Contiene Subpartida + Partida Principal + {:else if invoiceType === 'REP' || invoiceType === 'REPAR'} + Línea + No. Factura Expo + Línea Expo + P/S + Clase + Num. Parte + Descripción en Español + Cantidad Importada + UM + Preferencia + Contiene Subpartida + Partida Principal + {:else} + Línea + P/S + Clase + Descripcion Clase + Cant. Importada + U.M. + Preferencia + Contiene Subpartida + Partida Principal + {/if} + Acciones {#if displayedItems.length === 0} - + No hay items disponibles {:else} {#each displayedItems as item (item.id)} - - {item.line_number} - {item.is_subitem ? 'S' : 'P'} - {item.class_code || '-'} - {item.class_description || '-'} - {item.quantity?.quantity || '0'} - {item.unit_of_measure_code || '-'} - {item.reference_number || '-'} - {item.fa_data?.contains_subitems ? 'Sí' : 'No'} - {item.warehouse || '-'} + handleRowClick(item)} + class="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.fa_data?.search_invoice || '-'} + {item.fa_data?.search_line || '-'} + {item.is_subitem ? 'S' : 'P'} + {item.quantity?.quantity || '0'} + {item.class_code || '-'} + {item.part_number || '-'} + + {item.description?.description_spanish || '-'} + + {item.fa_data?.contains_subitems ? 'Sí' : 'No'} + {item.warehouse || '-'} + {:else if invoiceType === 'CR'} + {item.line_number} + {item.fa_data?.search_invoice || '-'} + {item.fa_data?.search_line || '-'} + {item.is_subitem ? 'S' : 'P'} + {item.quantity?.quantity || '0'} + {item.class_code || '-'} + {item.part_number || '-'} + + {item.description?.description_spanish || '-'} + + {item.fa_data?.contains_subitems ? 'Sí' : 'No'} + {item.warehouse || '-'} + {:else if invoiceType === 'REP' || invoiceType === 'REPAR'} + {item.line_number} + {item.fa_data?.search_invoice || '-'} + {item.fa_data?.search_line || '-'} + {item.is_subitem ? 'S' : 'P'} + {item.class_code || '-'} + {item.part_number || '-'} + + {item.description?.description_spanish || '-'} + + {item.quantity?.quantity || '0'} + {item.unit_of_measure_code || '-'} + {item.reference_number || '-'} + {item.fa_data?.contains_subitems ? 'Sí' : 'No'} + {item.warehouse || '-'} + {:else} + {item.line_number} + {item.is_subitem ? 'S' : 'P'} + {item.class_code || '-'} + {item.class_description || '-'} + {item.quantity?.quantity || '0'} + {item.unit_of_measure_code || '-'} + {item.reference_number || '-'} + {item.fa_data?.contains_subitems ? 'Sí' : 'No'} + {item.warehouse || '-'} + {/if}
@@ -760,7 +867,7 @@ {/each} {#if isLoadingMore} - + Cargando más items... @@ -771,43 +878,66 @@
{#if flattenedLines.length > 0} -
+
Mostrando {displayedItems.length} de {flattenedLines.length} líneas
{/if}
-
-
-

Cantidades:

-
-
-
- Partidas: {items.length || 0} -
-
- Bultos: 0 -
+
+ {#if operationType === 1} + +
+

+ Descripción en español: +

+
+ {#if focusedLine} +

+ {focusedLine.description?.description_spanish || 'Sin descripción disponible.'} +

+ {:else} +

+ Selecciona una fila para ver la descripción. +

+ {/if}
- Importada:{imported || 0}
- Peso neto: {net_weight || 0}
- Peso bruto: {gross_weight || 0}
+ {/if} + + +
+
+

Cantidades:

+
+
+
+ Partidas: {items.length || 0} +
+
+ Bultos: 0 +
+
+
+ Importada:{imported || 0}
+ Peso neto: {net_weight || 0}
+ Peso bruto: {gross_weight || 0}
+
+ +

+ Valores de importacion: +

+ Dolares:0 USD
+ Pesos: 0 MXN
+ De Captura: 0 USD + +

+ spacer +

+ + Aduana:0 USD
+ Aduana: 0 MXN
- -

- Valores de importacion: -

- Dolares:0 USD
- Pesos: 0 MXN
- De Captura: 0 USD - -

- spacer -

- - Aduana:0 USD
- Aduana: 0 MXN
@@ -850,7 +980,7 @@