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()}
+ />
+
+
+
+
+
+
+
+
+ | Número de Manifiesto |
+ Descripción |
+
+
+
+ {#if manifests.length === 0}
+
+ |
+ {#if loading}
+ Buscando manifiestos...
+ {:else}
+ No se encontraron resultados
+ {/if}
+ |
+
+ {:else}
+ {#each manifests as manifest}
+ handleSelect(manifest)}
+ >
+ |
+ {manifest.manifest_number}
+ |
+
+ {manifest.description || '-'}
+ |
+
+ {/each}
+ {/if}
+
+
+
+
+
+
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}
+
+
+
+ | Código |
+ Nombre / Sección |
+ |
+
+
+
+ {#each filteredSections as section}
+ handleSelect(section)}>
+ | {section.customs_code} |
+ {section.section_name} |
+
+
+ |
+
+ {/each}
+
+
+ {: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
-
+
-
-
+
-
-
-
-
+
+
+
+
+
+
+
+ {#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
-
-
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
+
+ {#if operationType === 1 || invoiceType === 'CR'}
+
+
+ {/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 ?? '';
- }}
- >
-
-
- {#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 ?? '';
+ }}
+ >
+
+
+ {#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 @@