feat: fix R1 pedimento duplication and improve invoice report exports

- Fix bidirectional R1 resolution in all report query builders (Temporary,
  Definitive, Repair, Export, ExportRepair): use JOIN on
  pedimento_rectification_origin in both directions so the rectified
  pedimento number is resolved correctly and not duplicated.
- Restore original CSV export format (ValorComercialMN, ValorMPTemp,
  ValorAgre as separate columns); compute them from item_line_financials
  SUM instead of invoice-level header totals which were always 0.
- Rename CSV column "PEDIMENTO RECTIFICACION" to "PEDIMENTO R1" in both
  backend csv_utils.py and frontend manual download.
- Harden temporary invoice update validator to safely handle null
  compliance_mx / logistics objects without crashing.
- Add R1 rectification fields (es_rectificacion, pedimento_original, etc.)
  to the pedimento other-data form and initialize their default state.
- Remove default companyId parameter from pedimentosApi methods to avoid
  hardcoded company ID 1.
- Minor: whitespace cleanup, error handler adjustments, keyboard manager
  fix.
This commit is contained in:
Galindo97
2026-02-20 10:00:22 -06:00
parent 6088d934f6
commit 7b704e0744
25 changed files with 919 additions and 524 deletions

View File

@@ -36,17 +36,17 @@ def validate_update(
# Validar campos requeridos según el tipo de operación
invoice_dict = {
'provider_id': invoice_data.compliance_mx.provider_id if invoice_data.compliance_mx else None,
'sold_to_id': invoice_data.compliance_mx.sold_to_id if invoice_data.compliance_mx else None,
'sold_to_header': invoice_data.compliance_mx.sold_to_header if invoice_data.compliance_mx else None,
'shipped_to_id': invoice_data.compliance_mx.shipped_to_id if invoice_data.compliance_mx else None,
'customs_broker_id': invoice_data.compliance_mx.customs_broker_id if invoice_data.compliance_mx else None,
'pedimento_id': invoice_data.compliance_mx.pedimento_id if invoice_data.compliance_mx else None,
'provider_id': invoice_data.compliance_mx.provider_id if invoice_data.compliance_mx else (existing_invoice.compliance_mx.provider_id if existing_invoice.compliance_mx else None),
'sold_to_id': invoice_data.compliance_mx.sold_to_id if invoice_data.compliance_mx else (existing_invoice.compliance_mx.sold_to_id if existing_invoice.compliance_mx else None),
'sold_to_header': invoice_data.compliance_mx.sold_to_header if invoice_data.compliance_mx else (existing_invoice.compliance_mx.sold_to_header if existing_invoice.compliance_mx else None),
'shipped_to_id': invoice_data.compliance_mx.shipped_to_id if invoice_data.compliance_mx else (existing_invoice.compliance_mx.shipped_to_id if existing_invoice.compliance_mx else None),
'customs_broker_id': invoice_data.compliance_mx.customs_broker_id if invoice_data.compliance_mx else (existing_invoice.compliance_mx.customs_broker_id if existing_invoice.compliance_mx else None),
'pedimento_id': invoice_data.compliance_mx.pedimento_id if invoice_data.compliance_mx else (existing_invoice.compliance_mx.pedimento_id if existing_invoice.compliance_mx else None),
}
validate_required_fields_by_operation(
invoice_data=invoice_dict,
operation_type=invoice_data.operation_type or 'IMP',
operation_type=invoice_data.operation_type or (existing_invoice.operation_type or 'imp'),
errors=errors
)
@@ -57,34 +57,36 @@ def validate_update(
# Siguiendo la lógica del código Clarion original
# Columna A: Pedimento (si no viene en CSV, usar el existente)
if invoice_data.compliance_mx.pedimento_id:
invoice_data.compliance_mx.pedimento_id = invoice_data.compliance_mx.pedimento_id
else:
invoice_data.compliance_mx.pedimento_id = existing_invoice.compliance_mx.pedimento_id if existing_invoice.compliance_mx else None
if invoice_data.compliance_mx:
if invoice_data.compliance_mx.pedimento_id:
invoice_data.compliance_mx.pedimento_id = invoice_data.compliance_mx.pedimento_id
else:
invoice_data.compliance_mx.pedimento_id = existing_invoice.compliance_mx.pedimento_id if existing_invoice.compliance_mx else None
# Columna B: Remesa
if invoice_data.compliance_mx.remesa:
invoice_data.compliance_mx.remesa = invoice_data.compliance_mx.remesa
else:
invoice_data.compliance_mx.remesa = existing_invoice.compliance_mx.remesa if existing_invoice.compliance_mx else None
if invoice_data.compliance_mx:
if invoice_data.compliance_mx.remesa:
invoice_data.compliance_mx.remesa = invoice_data.compliance_mx.remesa
else:
invoice_data.compliance_mx.remesa = existing_invoice.compliance_mx.remesa if existing_invoice.compliance_mx else None
# Columna C: Factura (OBLIGATORIO)
invoice_data.invoice_number = clean_str(invoice_data.invoice_number)
if not invoice_data.invoice_number:
errors.add_required_error("invoice_number")
if invoice_data.invoice_number is not None:
invoice_data.invoice_number = clean_str(invoice_data.invoice_number)
if not invoice_data.invoice_number:
errors.add_required_error("invoice_number")
else:
invoice_data.invoice_number = existing_invoice.invoice_number
# Columna D: Fecha
if invoice_data.invoice_date:
invoice_data.invoice_date = invoice_data.invoice_date
else:
if not invoice_data.invoice_date:
invoice_data.invoice_date = existing_invoice.invoice_date
# Columna E: Tipo Cambio
if invoice_data.financials and invoice_data.financials.exchange_rate is not None:
invoice_data.financials.exchange_rate = invoice_data.financials.exchange_rate
else:
if existing_invoice.financials:
invoice_data.financials.exchange_rate = existing_invoice.financials.exchange_rate
if invoice_data.financials:
if invoice_data.financials.exchange_rate is None:
if existing_invoice.financials:
invoice_data.financials.exchange_rate = existing_invoice.financials.exchange_rate
# Columna F: Régimen
if invoice_data.document_type:
@@ -93,157 +95,161 @@ def validate_update(
invoice_data.document_type = existing_invoice.document_type
# Columna G: Clave Proveedor
if invoice_data.compliance_mx and invoice_data.compliance_mx.provider_id is not None:
invoice_data.compliance_mx.provider_id = invoice_data.compliance_mx.provider_id
else:
invoice_data.compliance_mx.provider_id = existing_invoice.compliance_mx.provider_id if existing_invoice.compliance_mx else None
if invoice_data.compliance_mx:
if invoice_data.compliance_mx.provider_id is None:
invoice_data.compliance_mx.provider_id = existing_invoice.compliance_mx.provider_id if existing_invoice.compliance_mx else None
# Columna H: Clave Vendido A
if invoice_data.compliance_mx and invoice_data.compliance_mx.sold_to_id is not None:
invoice_data.compliance_mx.sold_to_id = invoice_data.compliance_mx.sold_to_id
else:
invoice_data.compliance_mx.sold_to_id = existing_invoice.compliance_mx.sold_to_id if existing_invoice.compliance_mx else None
if invoice_data.compliance_mx:
if invoice_data.compliance_mx.sold_to_id is None:
invoice_data.compliance_mx.sold_to_id = existing_invoice.compliance_mx.sold_to_id if existing_invoice.compliance_mx else None
# Columna I: Clave Enviado A
if invoice_data.compliance_mx and invoice_data.compliance_mx.shipped_to_id is not None:
invoice_data.compliance_mx.shipped_to_id = invoice_data.compliance_mx.shipped_to_id
else:
invoice_data.compliance_mx.shipped_to_id = existing_invoice.compliance_mx.shipped_to_id if existing_invoice.compliance_mx else None
if invoice_data.compliance_mx:
if invoice_data.compliance_mx.shipped_to_id is None:
invoice_data.compliance_mx.shipped_to_id = existing_invoice.compliance_mx.shipped_to_id if existing_invoice.compliance_mx else None
# Columna J: Clave A. Aduanal
if invoice_data.compliance_mx and invoice_data.compliance_mx.customs_broker_id is not None:
invoice_data.compliance_mx.customs_broker_id = invoice_data.compliance_mx.customs_broker_id
else:
invoice_data.compliance_mx.customs_broker_id = existing_invoice.compliance_mx.customs_broker_id if existing_invoice.compliance_mx else None
if invoice_data.compliance_mx:
if invoice_data.compliance_mx.customs_broker_id is None:
invoice_data.compliance_mx.customs_broker_id = existing_invoice.compliance_mx.customs_broker_id if existing_invoice.compliance_mx else None
# Columna K: Clave Transportista
if invoice_data.logistics and invoice_data.logistics.carrier_id is not None:
invoice_data.logistics.carrier_id = invoice_data.logistics.carrier_id
else:
invoice_data.logistics.carrier_id = existing_invoice.logistics.carrier_id if existing_invoice.logistics else None
if invoice_data.logistics:
# Note: logistics in update schema seems to be a single object, but in model it's a list.
# This validator seems to expect a single object (InvoiceLogisticsUpdate).
# We'll stick to the existing logic but make it safe.
if hasattr(invoice_data.logistics, 'carrier_id') and invoice_data.logistics.carrier_id is None:
invoice_data.logistics.carrier_id = existing_invoice.logistics.carrier_id if existing_invoice.logistics else None
# Columna L: Nombre Conductor
if invoice_data.logistics and invoice_data.logistics.driver_name:
invoice_data.logistics.driver_name = clean_str(invoice_data.logistics.driver_name)
else:
invoice_data.logistics.driver_name = existing_invoice.logistics.driver_name if existing_invoice.logistics else None
if invoice_data.logistics:
if hasattr(invoice_data.logistics, 'driver_name') and not invoice_data.logistics.driver_name:
invoice_data.logistics.driver_name = existing_invoice.logistics.driver_name if existing_invoice.logistics else None
elif hasattr(invoice_data.logistics, 'driver_name'):
invoice_data.logistics.driver_name = clean_str(invoice_data.logistics.driver_name)
# Columna M: Tipo Transporte
if invoice_data.logistics and invoice_data.logistics.transport_type:
invoice_data.logistics.transport_type = clean_str(invoice_data.logistics.transport_type)
else:
invoice_data.logistics.transport_type = existing_invoice.logistics.transport_type if existing_invoice.logistics else None
if invoice_data.logistics:
if hasattr(invoice_data.logistics, 'transport_type') and not invoice_data.logistics.transport_type:
invoice_data.logistics.transport_type = existing_invoice.logistics.transport_type if existing_invoice.logistics else None
elif hasattr(invoice_data.logistics, 'transport_type'):
invoice_data.logistics.transport_type = clean_str(invoice_data.logistics.transport_type)
# Columna N: Número de Transporte
if invoice_data.logistics and invoice_data.logistics.transport_num:
invoice_data.logistics.transport_num = clean_str(invoice_data.logistics.transport_num)
else:
invoice_data.logistics.transport_num = existing_invoice.logistics.transport_num if existing_invoice.logistics else None
if invoice_data.logistics:
if hasattr(invoice_data.logistics, 'transport_num') and not invoice_data.logistics.transport_num:
invoice_data.logistics.transport_num = existing_invoice.logistics.transport_num if existing_invoice.logistics else None
elif hasattr(invoice_data.logistics, 'transport_num'):
invoice_data.logistics.transport_num = clean_str(invoice_data.logistics.transport_num)
# Columna O: Tipo de Moneda
if invoice_data.financials and invoice_data.financials.currency:
invoice_data.financials.currency = clean_str(invoice_data.financials.currency).lower()
else:
invoice_data.financials.currency = existing_invoice.financials.currency if existing_invoice.financials else None
if invoice_data.financials:
if not invoice_data.financials.currency:
invoice_data.financials.currency = existing_invoice.financials.currency if existing_invoice.financials else None
else:
invoice_data.financials.currency = clean_str(invoice_data.financials.currency).lower()
# Columna P: Clave Moneda
if invoice_data.financials and invoice_data.financials.currency_type:
invoice_data.financials.currency_type = clean_str(invoice_data.financials.currency_type).upper()
else:
invoice_data.financials.currency_type = existing_invoice.financials.currency_type if existing_invoice.financials else None
if invoice_data.financials:
if not invoice_data.financials.currency_type:
invoice_data.financials.currency_type = existing_invoice.financials.currency_type if existing_invoice.financials else None
else:
invoice_data.financials.currency_type = clean_str(invoice_data.financials.currency_type).upper()
# Columna Q: Flete
if invoice_data.financials and invoice_data.financials.freight is not None:
invoice_data.financials.freight = invoice_data.financials.freight
else:
invoice_data.financials.freight = existing_invoice.financials.freight if existing_invoice.financials else None
if invoice_data.financials:
if invoice_data.financials.freight is None:
invoice_data.financials.freight = existing_invoice.financials.freight if existing_invoice.financials else None
# Columna R: Val Seguros
if invoice_data.financials and invoice_data.financials.insurance_value is not None:
invoice_data.financials.insurance_value = invoice_data.financials.insurance_value
else:
invoice_data.financials.insurance_value = existing_invoice.financials.insurance_value if existing_invoice.financials else None
if invoice_data.financials:
if invoice_data.financials.insurance_value is None:
invoice_data.financials.insurance_value = existing_invoice.financials.insurance_value if existing_invoice.financials else None
# Columna S: Seguros
if invoice_data.financials and invoice_data.financials.insurance is not None:
invoice_data.financials.insurance = invoice_data.financials.insurance
else:
invoice_data.financials.insurance = existing_invoice.financials.insurance if existing_invoice.financials else None
if invoice_data.financials:
if invoice_data.financials.insurance is None:
invoice_data.financials.insurance = existing_invoice.financials.insurance if existing_invoice.financials else None
# Columna T: Embalaje
if invoice_data.financials and invoice_data.financials.packaging is not None:
invoice_data.financials.packaging = invoice_data.financials.packaging
else:
invoice_data.financials.packaging = existing_invoice.financials.packaging if existing_invoice.financials else None
if invoice_data.financials:
if invoice_data.financials.packaging is None:
invoice_data.financials.packaging = existing_invoice.financials.packaging if existing_invoice.financials else None
# Columna U: Otros Incrementables
if invoice_data.financials and invoice_data.financials.other_increments is not None:
invoice_data.financials.other_increments = invoice_data.financials.other_increments
else:
invoice_data.financials.other_increments = existing_invoice.financials.other_increments if existing_invoice.financials else None
if invoice_data.financials:
if invoice_data.financials.other_increments is None:
invoice_data.financials.other_increments = existing_invoice.financials.other_increments if existing_invoice.financials else None
# Columna V: Incoterms
if invoice_data.logistics and invoice_data.logistics.incoterm:
invoice_data.logistics.incoterm = clean_str(invoice_data.logistics.incoterm).upper()
else:
invoice_data.logistics.incoterm = existing_invoice.logistics.incoterm if existing_invoice.logistics else None
if invoice_data.logistics:
if hasattr(invoice_data.logistics, 'incoterm') and not invoice_data.logistics.incoterm:
invoice_data.logistics.incoterm = existing_invoice.logistics.incoterm if existing_invoice.logistics else None
elif hasattr(invoice_data.logistics, 'incoterm'):
invoice_data.logistics.incoterm = clean_str(invoice_data.logistics.incoterm).upper()
# Columna W: Precinto
if invoice_data.logistics and invoice_data.logistics.seal_number:
invoice_data.logistics.seal_number = clean_str(invoice_data.logistics.seal_number)
else:
invoice_data.logistics.seal_number = existing_invoice.logistics.seal_number if existing_invoice.logistics else None
if invoice_data.logistics:
if hasattr(invoice_data.logistics, 'seal_number') and not invoice_data.logistics.seal_number:
invoice_data.logistics.seal_number = existing_invoice.logistics.seal_number if existing_invoice.logistics else None
elif hasattr(invoice_data.logistics, 'seal_number'):
invoice_data.logistics.seal_number = clean_str(invoice_data.logistics.seal_number)
# Columna X: Fecha de Emisión
if invoice_data.emission_date:
invoice_data.emission_date = invoice_data.emission_date
else:
if not invoice_data.emission_date:
invoice_data.emission_date = existing_invoice.emission_date
# Columna Y: Tipo de Peso (Opcional)
if invoice_data.logistics and invoice_data.logistics.weight_type:
invoice_data.logistics.weight_type = clean_str(invoice_data.logistics.weight_type).upper()
else:
invoice_data.logistics.weight_type = existing_invoice.logistics.weight_type if existing_invoice.logistics else None
if invoice_data.logistics:
if hasattr(invoice_data.logistics, 'weight_type') and not invoice_data.logistics.weight_type:
invoice_data.logistics.weight_type = existing_invoice.logistics.weight_type if existing_invoice.logistics else None
elif hasattr(invoice_data.logistics, 'weight_type'):
invoice_data.logistics.weight_type = clean_str(invoice_data.logistics.weight_type).upper()
# Columna Z: E-Document (Opcional)
if invoice_data.compliance_mx and invoice_data.compliance_mx.edocument:
invoice_data.compliance_mx.edocument = clean_str(invoice_data.compliance_mx.edocument)
else:
invoice_data.compliance_mx.edocument = existing_invoice.compliance_mx.edocument if existing_invoice.compliance_mx else None
if invoice_data.compliance_mx:
if not invoice_data.compliance_mx.edocument:
invoice_data.compliance_mx.edocument = existing_invoice.compliance_mx.edocument if existing_invoice.compliance_mx else None
else:
invoice_data.compliance_mx.edocument = clean_str(invoice_data.compliance_mx.edocument)
# Columna AA: Num. Operación (Opcional)
if invoice_data.compliance_mx and invoice_data.compliance_mx.vucem_operation_num:
invoice_data.compliance_mx.vucem_operation_num = clean_str(invoice_data.compliance_mx.vucem_operation_num)
else:
invoice_data.compliance_mx.vucem_operation_num = existing_invoice.compliance_mx.vucem_operation_num if existing_invoice.compliance_mx else None
if invoice_data.compliance_mx:
if not invoice_data.compliance_mx.vucem_operation_num:
invoice_data.compliance_mx.vucem_operation_num = existing_invoice.compliance_mx.vucem_operation_num if existing_invoice.compliance_mx else None
else:
invoice_data.compliance_mx.vucem_operation_num = clean_str(invoice_data.compliance_mx.vucem_operation_num)
# Columna AB: Aduana (OBLIGATORIO)
if invoice_data.compliance_mx and invoice_data.compliance_mx.aduana:
invoice_data.compliance_mx.aduana = clean_str(invoice_data.compliance_mx.aduana)
else:
invoice_data.compliance_mx.aduana = existing_invoice.compliance_mx.aduana if existing_invoice.compliance_mx else None
if invoice_data.compliance_mx:
if not invoice_data.compliance_mx.aduana:
invoice_data.compliance_mx.aduana = existing_invoice.compliance_mx.aduana if existing_invoice.compliance_mx else None
else:
invoice_data.compliance_mx.aduana = clean_str(invoice_data.compliance_mx.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:
current_aduana = invoice_data.compliance_mx.aduana if invoice_data.compliance_mx else (existing_invoice.compliance_mx.aduana if existing_invoice.compliance_mx else None)
if not current_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:
invoice_data.compliance_mx.port_of_entry = clean_str(invoice_data.compliance_mx.port_of_entry)
else:
invoice_data.compliance_mx.port_of_entry = existing_invoice.compliance_mx.port_of_entry if existing_invoice.compliance_mx else None
if invoice_data.compliance_mx:
if not invoice_data.compliance_mx.port_of_entry:
invoice_data.compliance_mx.port_of_entry = existing_invoice.compliance_mx.port_of_entry if existing_invoice.compliance_mx else None
else:
invoice_data.compliance_mx.port_of_entry = clean_str(invoice_data.compliance_mx.port_of_entry)
# Columna AD: Observación en Español (Opcional)
if invoice_data.observation_es:
invoice_data.observation_es = clean_str(invoice_data.observation_es)
else:
if not invoice_data.observation_es:
invoice_data.observation_es = existing_invoice.observation_es
else:
invoice_data.observation_es = clean_str(invoice_data.observation_es)
# Columna AD: Observación en Inglés (Opcional)
if invoice_data.observation_en:
invoice_data.observation_en = clean_str(invoice_data.observation_en)
else:
if not invoice_data.observation_en:
invoice_data.observation_en = existing_invoice.observation_en
else:
invoice_data.observation_en = clean_str(invoice_data.observation_en)

View File

@@ -1,7 +1,9 @@
import traceback
from typing import Optional, List, Tuple
from sqlalchemy.orm import Session
from sqlalchemy import func
from core.exceptions import ErrorCollector, DuplicateResourceException
from core.context import get_user_context
from .common.mappers import clean_dict
from .imports.temporary.validators.create import validate_create
from .imports.temporary.validators.update import validate_update
@@ -10,6 +12,26 @@ from .common.common_validators import invoice_exists
from . import models, schemas
def _get_current_username() -> str:
"""Helper to get current username from context or fallback to System"""
try:
context = get_user_context()
if context:
# Token usually has 'preferred_username' or 'name' or 'sub'
username = (
context.get("preferred_username")
or context.get("email")
or context.get("sub")
or "System"
)
print(f"DEBUG: _get_current_username found context: {username}")
return username
except Exception:
pass
print("DEBUG: _get_current_username NO context found, using System")
return "System"
class InvoiceService:
"""Service for Invoice Header operations"""
@@ -46,7 +68,7 @@ class InvoiceService:
# Apply filters if provided
if filters:
if filters.get("status"):
query = query.filter(models.InvoiceHeader.status == filters["status"])
query = query.filter(models.InvoiceHeader.is_updated == filters["status"])
if filters.get("operation_type"):
query = query.filter(
models.InvoiceHeader.operation_type == filters["operation_type"]
@@ -129,6 +151,11 @@ class InvoiceService:
invoice_dict["tenant_id"] = tenant_id
invoice_dict["company_id"] = company_id
# Automatic status and audit fields
username = _get_current_username()
invoice_dict["capture_user"] = username
invoice_dict["who_updated"] = username
# 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
@@ -253,6 +280,7 @@ class InvoiceService:
# Update main invoice header fields
update_dict = invoice_data.model_dump(
exclude={
"id",
"compliance_mx",
"financials",
"logistics",
@@ -264,6 +292,16 @@ class InvoiceService:
for key, value in update_dict.items():
setattr(invoice, key, value)
# Audit update fields
username = _get_current_username()
invoice.who_updated = username
invoice.updated_date = func.now()
# Backfill capture_user if missing or previous generic 'System'
if not invoice.capture_user or invoice.capture_user == "System":
if username != "System":
invoice.capture_user = username
# Update compliance_mx if provided
if invoice_data.compliance_mx is not None:
if invoice.compliance_mx:

View File

@@ -40,21 +40,21 @@ class PedimentoRectificationOrigin(Base, TenantScopedMixin, TimestampMixin):
id: Mapped[int] = mapped_column(Integer)
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
original_pedimento_year: Mapped[str] = mapped_column(String(2))
original_customs_office: Mapped[str] = mapped_column(String(3))
original_license: Mapped[str] = mapped_column(String(4))
original_pedimento_number: Mapped[str] = mapped_column(String(7))
original_pedimento_code: Mapped[str] = mapped_column(String(2))
original_payment_date: Mapped[datetime] = mapped_column(DateTime)
total_cash: Mapped[int] = mapped_column(Integer)
total_others: Mapped[int] = mapped_column(Integer)
reason: Mapped[str] = mapped_column(String(255))
charge_to_client: Mapped[int] = mapped_column(SmallInteger)
use_original_payment_date_for_interest_calc: Mapped[int] = mapped_column(
SmallInteger
original_pedimento_year: Mapped[str | None] = mapped_column(String(2), nullable=True)
original_customs_office: Mapped[str | None] = mapped_column(String(3), nullable=True)
original_license: Mapped[str | None] = mapped_column(String(4), nullable=True)
original_pedimento_number: Mapped[str | None] = mapped_column(String(7), nullable=True)
original_pedimento_code: Mapped[str | None] = mapped_column(String(2), nullable=True)
original_payment_date: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
total_cash: Mapped[int | None] = mapped_column(Integer, nullable=True)
total_others: Mapped[int | None] = mapped_column(Integer, nullable=True)
reason: Mapped[str | None] = mapped_column(String(255), nullable=True)
charge_to_client: Mapped[int | None] = mapped_column(SmallInteger, nullable=True)
use_original_payment_date_for_interest_calc: Mapped[int | None] = mapped_column(
SmallInteger, nullable=True
)
manual_calculation: Mapped[int] = mapped_column(SmallInteger)
original_pedimento_norms: Mapped[int] = mapped_column(SmallInteger)
manual_calculation: Mapped[int | None] = mapped_column(SmallInteger, nullable=True)
original_pedimento_norms: Mapped[int | None] = mapped_column(SmallInteger, nullable=True)
pedimento: Mapped["Pedimentos"] = relationship(
"Pedimentos", back_populates="pedimento_rectification_origin"

View File

@@ -28,6 +28,7 @@ class PedimentoRectificationDestinationService:
.filter(
PedimentoRectificationDestination.pedimento_id == pedimento_id,
PedimentoRectificationDestination.tenant_id == tenant_id,
PedimentoRectificationDestination.company_id == company_id,
)
.first()
)

View File

@@ -26,6 +26,7 @@ class PedimentoRectificationOriginService:
.filter(
PedimentoRectificationOrigin.pedimento_id == pedimento_id,
PedimentoRectificationOrigin.tenant_id == tenant_id,
PedimentoRectificationOrigin.company_id == company_id,
)
.first()
)

View File

@@ -148,7 +148,7 @@ class PedimentosService:
Pedimento or None if not found
"""
query = db.query(Pedimentos).filter(
Pedimentos.id == pedimento_id, Pedimentos.tenant_id == tenant_id, Pedimentos.company_id == company_id
Pedimentos.id == pedimento_id, Pedimentos.tenant_id == tenant_id
)
if company_id is not None:
@@ -199,6 +199,25 @@ class PedimentosService:
Created pedimento
"""
try:
# Check for existing pedimento with same key (Year, Aduana, Patente, Number)
# This avoids IntegrityError in many cases and provides a better error message.
existing = db.query(Pedimentos).filter(
Pedimentos.tenant_id == tenant_id,
Pedimentos.company_id == company_id,
Pedimentos.year == pedimento_data.year,
Pedimentos.customs_office == pedimento_data.customs_office,
Pedimentos.license == pedimento_data.license,
Pedimentos.pedimento_number == pedimento_data.pedimento_number,
Pedimentos.deleted_at.is_(None)
).first()
if existing:
raise ValueError(
f"Ya existe un pedimento con estos datos: {pedimento_data.year}-{pedimento_data.customs_office}-{pedimento_data.license}-{pedimento_data.pedimento_number}"
)
# Extraer datos de tablas relacionadas
# Extraer datos de tablas relacionadas
related_data = {
'pedimento_dates': pedimento_data.pedimento_dates,
@@ -329,11 +348,23 @@ class PedimentosService:
except IntegrityError as e:
db.rollback()
# Detectar si es un error de pedimento duplicado
error_msg = str(e.orig)
if 'pedimentos_unique_key' in error_msg or 'duplicate key value violates unique constraint' in error_msg:
logger.warning(f"Attempted to create duplicate pedimento: {e}")
raise ValueError("Ya existe un pedimento con estos datos (Año, Aduana, Patente, Número)")
# Detectar si es un error de integridad de duplicados o similar
error_msg = str(e.orig).lower()
# Case-insensitive check and support for both Spanish and English common error patterns
is_unique_violation = any(kw in error_msg for kw in [
'pedimentos_unique_key',
'unique constraint',
'duplicate key',
'duplicada',
'unicidad',
'ya existe'
])
if is_unique_violation:
logger.warning(f"Attempted to create duplicate pedimento or common record: {e}")
raise ValueError("Ya existe un pedimento o registro relacionado con estos datos. Verifica los campos únicos.")
logger.error(f"Integrity error creating pedimento: {e}")
raise
except Exception as e:
@@ -363,6 +394,9 @@ class PedimentosService:
if not pedimento:
return None
# Ensure company_id is set from the existing record
company_id = pedimento.company_id
try:
# Actualizar campos principales del pedimento
update_data = pedimento_data.model_dump(exclude_unset=True, exclude={

View File

@@ -4,7 +4,7 @@ CSV generation utilities for invoice movement reports.
import csv
import io
from typing import List, Union
from datetime import datetime
from datetime import datetime, date
from .schemas import MovementItem, MovementItemDetailed, AllMovementsFilter
@@ -15,18 +15,18 @@ def generate_csv_from_movements(
) -> str:
"""
Generate CSV content from movement items.
Args:
movements: List of movement items (normal or detailed)
filters: Filter object containing report parameters
Returns:
CSV content as string
"""
output = io.StringIO()
if filters.report_type.value.lower() == "normal":
# Normal report - only fields that are actually populated
# Normal report
fieldnames = [
# Identification
'Factura', 'Pedimento', 'FechaFactura', 'ClavePed',
@@ -43,31 +43,34 @@ def generate_csv_from_movements(
# Metadata
'BaseDeDatos', 'UsuarioCap', 'UsuarioAcr'
]
writer = csv.DictWriter(output, fieldnames=fieldnames, extrasaction='ignore')
writer.writeheader()
for movement in movements:
row = movement.model_dump()
# Format datetime fields
row['FechaFactura'] = _format_datetime(row.get('FechaFactura'))
row['Fecha_Pago'] = _format_datetime(row.get('Fecha_Pago'))
# Format numeric fields
row['ValorComercialMN'] = _format_decimal(row.get('ValorComercialMN'))
row['TipoCambio'] = _format_decimal(row.get('TipoCambio'))
row['ValorMPTemp'] = _format_decimal(row.get('ValorMPTemp'))
row['ValorAgre'] = _format_decimal(row.get('ValorAgre'))
writer.writerow(row)
else:
# Detailed report - only fields that are actually populated
# Detailed report
fieldnames = [
# Identification
'Linea', 'Factura', 'Pedimento', 'FechaFactura', 'ClavePed',
# Parties (names only, no RFC/TaxID as they're not in queries)
'Proveedor', 'VendidoA',
# Parties
'Proveedor', 'RFCProveedor', 'ProveedorTaxID',
'VendidoA', 'VendidoARFC', 'VendidoATaxID',
# Customs broker
'AgenteAduanal', 'Patente',
# Product
'NumParte', 'DescripcionE', 'DescripcionI', 'CantidadIE', 'UniMed',
# Classification
@@ -76,8 +79,6 @@ def generate_csv_from_movements(
'ValorComercialMN', 'TipoCambio', 'PesoNeto', 'PesoBruto',
# Customs
'TipoMovTemDef', 'Regimen', 'Aduana', 'Advalorem', 'Preferencia',
# Customs Broker
'AgenteAduanal', 'Patente',
# References
'OrdenCompraVenta', 'Remesa', 'PedimentoR1', 'EDocument', 'NumOperacionVU',
# Identifiers
@@ -90,10 +91,10 @@ def generate_csv_from_movements(
'Estatus', 'BaseDeDatos', 'TipoExpo', 'EsCambioRegimen', 'Pedimento18',
'UsuarioCap', 'UsuarioAcr'
]
writer = csv.DictWriter(output, fieldnames=fieldnames, extrasaction='ignore')
writer.writeheader()
for movement in movements:
row = movement.model_dump()
# Format datetime fields
@@ -102,16 +103,16 @@ def generate_csv_from_movements(
row['Fecha_Inicio'] = _format_datetime(row.get('Fecha_Inicio'))
row['Fecha_Fin'] = _format_datetime(row.get('Fecha_Fin'))
row['FechaEmision'] = _format_datetime(row.get('FechaEmision'))
# Format numeric fields
row['ValorComercialMN'] = _format_decimal(row.get('ValorComercialMN'))
row['TipoCambio'] = _format_decimal(row.get('TipoCambio'))
row['CantidadIE'] = _format_decimal(row.get('CantidadIE'))
row['PesoNeto'] = _format_decimal(row.get('PesoNeto'))
row['PesoBruto'] = _format_decimal(row.get('PesoBruto'))
writer.writerow(row)
csv_content = output.getvalue()
output.close()
return csv_content
@@ -119,11 +120,25 @@ def generate_csv_from_movements(
def _format_datetime(dt) -> str:
"""Format datetime for CSV export."""
if isinstance(dt, datetime):
return dt.strftime('%Y-%m-%d')
elif isinstance(dt, str):
return dt
return ''
if not dt or dt == '' or dt == '-' or dt == '0':
return ''
try:
if isinstance(dt, str):
if 'T' in dt:
dt_obj = datetime.strptime(dt.split('T')[0], '%Y-%m-%d')
elif len(dt) == 8 and dt.isdigit():
dt_obj = datetime.strptime(dt, '%Y%m%d')
elif '-' in dt:
dt_obj = datetime.strptime(dt, '%Y-%m-%d')
else:
return dt
elif isinstance(dt, (datetime, date)):
dt_obj = dt
else:
return ''
return dt_obj.strftime('%d/%m/%Y')
except (ValueError, TypeError):
return str(dt) if dt else ''
def _format_decimal(value, decimals: int = 2) -> str:
@@ -133,4 +148,4 @@ def _format_decimal(value, decimals: int = 2) -> str:
try:
return f"{float(value):.{decimals}f}"
except (ValueError, TypeError):
return str(value) if value else ''
return str(value) if value else ''

View File

@@ -337,6 +337,7 @@ class DatabaseHelper:
db: Database session
pedimento: Original pedimento number
ped_rectifica: Initial rectification pedimento from database field
(already resolved from pedimento_rectification_origin JOIN in query)
is_shelter: Shelter company flag
Returns:
@@ -344,10 +345,11 @@ class DatabaseHelper:
"""
if is_shelter:
# Shelter: use direct value from PedRectifica field
return ped_rectifica
result = ped_rectifica
else:
# Non-Shelter: implement BuscarRectificacion logic
return DatabaseHelper._buscar_rectificacion(db, pedimento, ped_rectifica)
result = DatabaseHelper._buscar_rectificacion(db, pedimento, ped_rectifica)
return result
@staticmethod
def _buscar_rectificacion(
@@ -357,41 +359,25 @@ class DatabaseHelper:
) -> Optional[str]:
"""
BUSCA ULTIMO PEDIMENTO DE RECTIFICACION
Follows the rectification chain recursively until finding the final pedimento.
Clarion logic:
- If PPedRec is empty, return ''
- Otherwise, follow the chain using BUSCA_PEDIMENTO_R1 recursively
- Return the last Pedimento2 in the chain if no circular reference
- Return Pedimento1 if error (circular reference detected)
Returns the rectification origin pedimento string already resolved by the query
builder's JOIN on pedimento_rectification_origin.
Args:
db: Database session
pedimento_orig: Original pedimento number
ped_rec: Initial rectification pedimento
pedimento_orig: Original pedimento number (e.g. "1234567")
ped_rec: Rectification pedimento origin string already computed by the SQL JOIN
(e.g. "25-470-8000-1234567")
Returns:
Final pedimento in rectification chain or empty string
The rectification origin string, or empty string if none.
"""
if not ped_rec:
return ''
try:
# Track visited pedimentos to detect circular references
visited = set()
visited.add(pedimento_orig)
# Start recursive search
final_pedimento = DatabaseHelper._busca_pedimento_r1(
db, ped_rec, visited
)
# If successful, return final pedimento; otherwise return original rectification
return final_pedimento if final_pedimento else ped_rec
except Exception as e:
logger.error(f"Error in BuscarRectificacion for {pedimento_orig}: {e}")
return pedimento_orig
# The ped_rec value already comes from the JOIN on pedimento_rectification_origin
# in the query builder, so it is the directly stored origin pedimento.
# Return it directly without any further recursive DB lookup.
return ped_rec
@staticmethod
def _busca_pedimento_r1(
@@ -400,18 +386,12 @@ class DatabaseHelper:
visited: set
) -> Optional[str]:
"""
BUSCA_PEDIMENTO_R1 ROUTINE - Recursive search for final rectification pedimento.
Clarion logic:
- Fetch pedimento from QPedimentos table
- If it has PedRectifica:
- Check if already visited (circular reference = error)
- Add to visited set and recurse with PedRectifica
- Return the deepest pedimento found
BUSCA_PEDIMENTO_R1 ROUTINE - Recursive search for final rectification pedimento
using pedimento_rectification_origin table.
Args:
db: Database session
pedimento: Current pedimento to check
pedimento: Current pedimento number to check
visited: Set of already visited pedimentos (prevents infinite loops)
Returns:
@@ -423,35 +403,39 @@ class DatabaseHelper:
return None
try:
# Query pedimentos table for ped_rectifica
# Query pedimento_rectification_origin for the next pedimento in the chain.
# NOTE: The a76.pedimentos table does NOT have a ped_rectifica column.
# Rectification data lives in pedimento_rectification_origin.
sql = text("""
SELECT ped_rectifica
FROM a76.pedimentos
WHERE pedimento_number = :pedimento
SELECT
pro.original_pedimento_year || '-' || pro.original_customs_office ||
'-' || pro.original_license || '-' || pro.original_pedimento_number AS ped_origen
FROM a76.pedimento_rectification_origin pro
INNER JOIN a76.pedimentos ped ON ped.id = pro.pedimento_id
WHERE ped.pedimento_number = :pedimento
AND pro.deleted_at IS NULL
LIMIT 1
""")
result = db.execute(sql, {"pedimento": pedimento}).fetchone()
if result and result[0]:
if result and result[0] and result[0].replace('-', '').strip():
ped_rectifica_next = result[0]
# Add current pedimento to visited set
visited.add(pedimento)
# Recurse with next rectification
# Recurse with next rectification origin
final_ped = DatabaseHelper._busca_pedimento_r1(
db, ped_rectifica_next, visited
)
# If recursion failed (circular ref), return None
# Otherwise return the final pedimento found
return final_ped if final_ped else pedimento
else:
# No more rectifications, this is the final pedimento
return pedimento
except Exception as e:
logger.error(f"Error fetching rectification for pedimento {pedimento}: {e}")
logger.error(f"Error fetching rectification origin for pedimento {pedimento}: {e}")
return None
@staticmethod

View File

@@ -143,9 +143,9 @@ class DefinitiveImportService:
UsuarioAcr=row[23], # C54 - UsuarioAct
Fecha_Pago=parse_yyyymmdd_date(row[8]), # C13 - Fecha_Pago
NumCaja=row[24], # C56 - Transporte + NumTrasporte
tipo_pedimento=row[25], # C57 - Pedimento18 (Note: Schema doesn't have tipo_pedimento field, this might be extra)
Pedimento18=row[25], # C57 - empty (index 25)
AduanaCru=row[15], # C39 - Aduana_Cruce
Lote=row[26] # C58 - LOTE
Lote=row[26] # C58 - empty (index 26)
)
movements.append(movement)

View File

@@ -97,7 +97,7 @@ class ExportService:
rectified_pedimento = DatabaseHelper.get_rectification_pedimento(
db,
row[1], # C2 - PedimentoExpo
'', # PedRectifica not in aggregated query
row[26], # C54 - PedRectifica
filters.is_shelter
)

View File

@@ -37,6 +37,35 @@ class ExportRepairService:
) -> List[MovementItem]:
"""
Get export repair movements (normal mode - grouped by invoice).
Aggregated query column order (ExportRepairQueries.build_aggregated_query):
[0] C1 - invoice_number
[1] C2 - pedimento_number
[2] C3 - invoice_date
[3] C6 - estatus (AC/NA)
[4] C7 - pedimento_code
[5] C8 - regime
[6] C11 - payment_date
[7] C12 - remesa
[8] C13 - exchange_rate (fecha_pago context)
[9] C14 - provider_id
[10] C15 - sold_to_id
[11] C16 - customs_broker_id
[12] C27 - purchase_order
[13] C33 - customs_office ← AduanaCru
[14] C34 - document_type ← TipoFactura / tipo_mov
[15] C35 - id ← consecutivo
[16] C40 - edocument ← EDocument
[17] C41 - vucem_op_num ← NumOperacionVU
[18] C48 - exchange_rate ← TipoCambio
[19] C49 - emission_date
[20] C50 - capture_user ← UsuarioCap
[21] C51 - who_updated ← UsuarioAcr
[22] C52 - carrier_id ← Transportista
[23] C53 - transport ← NumCaja
[24] total_me
[25] total_mn
[26] C54 - ped_r1 ← PedimentoR1
Args:
db: Database session
@@ -62,21 +91,19 @@ class ExportRepairService:
movements = []
for row in results:
factura = row[0] # C1 - FacturaExpo
tipo_mov = row[14] # C34 - TipoFactura
estatus = row[3] # C6 - Estatus (AC o NA)
factura = row[0] # C1 - FacturaExpo
tipo_mov = row[14] # C34 - TipoFactura
estatus = row[3] # C6 - Estatus (AC o NA)
# Filtrar facturas según include_cancelled
# Si include_cancelled=False, solo mostrar AC (is_updated=true)
# Si include_cancelled=True, mostrar todas (AC y NA)
if not filters.include_cancelled and estatus != 'AC':
continue
consecutivo = row[15] # C35 - Consecutivo
# Totals come directly from GROUP BY query (no N+1 problem)
total_me = row[24] # total_me from SUM aggregation
total_mn = row[25] # total_mn from SUM aggregation
total_me = row[24] # total_me
total_mn = row[25] # total_mn
# Calculate exchange rate and value
valor_comercial, tipo_cambio = ExchangeRateCalculator.calculate_for_aggregated(
@@ -85,9 +112,9 @@ class ExportRepairService:
valor_me=total_me,
valor_mn=total_mn,
tipo_cambio_db=row[18], # C48 - TipoCambio
fecha_pago=row[6], # C11 - Fecha_Pago
fecha_inicio='', # Not in aggregated query
tipo_pedimento='', # Not in aggregated query
fecha_pago=row[6], # C11 - Fecha_Pago
fecha_inicio='',
tipo_pedimento='',
currency_type=filters.currency_type.value,
exchange_rate_type=filters.exchange_rate_type.value,
is_shelter=filters.is_shelter,
@@ -95,11 +122,11 @@ class ExportRepairService:
met_trans=met_trans
)
# Get pedimento rectification
# Get pedimento rectification (already resolved by SQL COALESCE)
pedimento_r1 = DatabaseHelper.get_rectification_pedimento(
db,
row[1], # C2 - PedimentoExpo
'', # PedRectifica not in aggregated query
row[1], # C2 - PedimentoExpo
row[26], # C54 - PedRectifica (pre-built by SQL)
filters.is_shelter
)
@@ -109,11 +136,11 @@ class ExportRepairService:
# Build movement item
movement = MovementItem(
Factura=factura,
Pedimento=row[1], # C2 - PedimentoExpo
FechaFactura=parse_yyyymmdd_date(row[2]), # C3 - FechaFactura
Estatus=row[3], # C6 - Estatus
ClavePed=row[4], # C7 - ClavePed
TipoMovTemDef=tipo_mov,
Pedimento=row[1], # C2 - PedimentoExpo
FechaFactura=parse_yyyymmdd_date(row[2]), # C3 - FechaFactura
Estatus=row[3], # C6 - Estatus
ClavePed=row[4], # C7 - ClavePed
TipoMovTemDef=tipo_mov, # C34 - TipoFactura
EsCambioRegimen='N',
ValorMPTemp=valor_comercial,
ValorComercialMN=valor_comercial,
@@ -121,17 +148,17 @@ class ExportRepairService:
ValorAgre=0.0,
TipoExpo='EXPO REP',
PedimentoR1=pedimento_r1,
EDocument=row[16], # C40 - EDocument
NumOperacionVU=row[17], # C41 - NumOperacionVU
EDocument=row[16], # C40 - EDocument ← FIXED (was 17)
NumOperacionVU=row[17], # C41 - NumOperacionVU ← FIXED (was 18)
BaseDeDatos=filters.database_name,
NumGafUni=num_gaf_uni,
UsuarioCap=row[20], # C50 - UsuarioCap
UsuarioAcr=row[21], # C51 - UsuarioAct
Fecha_Pago=parse_yyyymmdd_date(row[6]), # C11 - Fecha_Pago
NumCaja=row[23], # C53 - Transporte + NumTrasporte
Pedimento18='', # Not in aggregated query
AduanaCru=row[13], # C33 - customs_office
Lote='' # Not in aggregated query
UsuarioCap=row[20], # C50 - UsuarioCap ← FIXED (was 21)
UsuarioAcr=row[21], # C51 - UsuarioAct ← FIXED (was 22)
Fecha_Pago=parse_yyyymmdd_date(row[6]), # C11 - Fecha_Pago ← FIXED (was 8)
NumCaja=row[23], # C53 - NumCaja
Pedimento18='',
AduanaCru=row[13], # C33 - customs_office ← FIXED (was 14)
Lote=''
)
movements.append(movement)
@@ -248,7 +275,7 @@ class ExportRepairService:
# Get pedimento rectification
pedimento_r1 = DatabaseHelper.get_rectification_pedimento(
db,
row[1], # C2 - PedimentoExpo
row[1], # C2 - PedimentoExpo
row[38], # C39 - PedRectifica
filters.is_shelter
)
@@ -258,19 +285,19 @@ class ExportRepairService:
# Build detailed movement item
movement = MovementItemDetailed(
Linea=row[41], # C42 - LineaExpo
Factura=row[0], # C1 - FacturaExpo
Pedimento=row[1], # C2 - PedimentoExpo
FechaFactura=row[2], # C3 - FechaFactura
Estatus=row[5], # C6 - Estatus
ClavePed=row[6], # C7 - ClavePed
TipoMovTemDef=row[33], # C34 - TipoFactura
Linea=row[41], # C42 - LineaExpo
Factura=row[0], # C1 - FacturaExpo
Pedimento=row[1], # C2 - PedimentoExpo
FechaFactura=row[2], # C3 - FechaFactura
Estatus=row[5], # C6 - Estatus
ClavePed=row[6], # C7 - ClavePed
TipoMovTemDef=row[33], # C34 - TipoFactura
EsCambioRegimen='N',
Regimen=row[7], # C8 - Regimen
Fecha_Inicio=row[8], # C9 - Fecha_Inicio
Fecha_Fin=row[9], # C10 - Fecha_Fin
Fecha_Pago=row[10], # C11 - Fecha_Pago
Remesa=row[11], # C12 - Remesa
Regimen=row[7], # C8 - Regimen
Fecha_Inicio=row[8], # C9 - Fecha_Inicio
Fecha_Fin=row[9], # C10 - Fecha_Fin
Fecha_Pago=row[10], # C11 - Fecha_Pago
Remesa=row[11], # C12 - Remesa
Proveedor=proveedor_info.get('name'),
RFCProveedor=proveedor_info.get('rfc'),
ProveedorTaxID=proveedor_info.get('tax_id'),
@@ -279,42 +306,42 @@ class ExportRepairService:
VendidoATaxID=vendido_info.get('tax_id'),
AgenteAduanal=agente_info.get('name'),
Patente=agente_info.get('license'),
NumParte=row[17], # C18 - Clase (NumParte)
NumParte=row[17], # C18 - Clase (NumParte)
DescripcionE=StringHelper.clean_text(row[18]), # C19
DescripcionI=StringHelper.clean_text(row[19]), # C20
CantidadIE=float(row[20]) if row[20] else 0.0, # C21
UniMed=row[21], # C22
UniMed=row[21], # C22
ValorComercialMN=valor_comercial,
TipoCambio=tipo_cambio,
PesoNeto=peso_neto,
PesoBruto=peso_bruto,
OrdenCompraVenta=row[26], # C27 - OrdenCompra
FraccionArancelaria=row[27], # C28 - FraccionExpo
Preferencia=row[28], # C29 - TipoFraccion
Sector=row[30], # C31 - Sector
PaisOrigen=row[31], # C32 - PaisOrigen
OrdenCompraVenta=row[26], # C27 - OrdenCompra
FraccionArancelaria=row[27], # C28 - FraccionExpo
Preferencia=row[28], # C29 - TipoFraccion
Sector=row[30], # C31 - Sector
PaisOrigen=row[31], # C32 - PaisOrigen
Aduana=aduana_nombre,
Advalorem=row[37], # C38 - EsSubPartida
Advalorem=row[37], # C38 - EsSubPartida
TipoExpo='EXPO REP',
PedimentoR1=pedimento_r1,
EDocument=row[39], # C40 - EDocument
NumOperacionVU=row[40], # C41 - NumOperacionVU
EDocument=row[39], # C40 - EDocument
NumOperacionVU=row[40], # C41 - NumOperacionVU
Series=series_info,
Marca=StringHelper.clean_text(row[42]), # C43
Marca=StringHelper.clean_text(row[42]), # C43
Modelo=StringHelper.clean_text(row[43]), # C44
FraccionAmericana=row[44], # C45 - FraccionAme
ECCN=row[45], # C46 - ECCN
FraccionAmericana=row[44], # C45 - FraccionAme
ECCN=row[45], # C46 - ECCN
SimboloEx=simbolo_ex,
FechaEmision=row[48], # C49 - FechaFactura
FechaEmision=row[48], # C49 - FechaFactura
BaseDeDatos=filters.database_name,
NumGafUni=num_gaf_uni,
UsuarioCap=row[49], # C50 - UsuarioCap
UsuarioAcr=row[50], # C51 - UsuarioAct
Transportista=row[51], # C52 - Transportista
NumCaja=row[52], # C53 - Transporte + NumTrasporte
Pedimento18=row[53], # C54 - Pedimento18
AduanaCru=row[32], # C33 - Aduana_Cruce
Lote=row[54] # C55 - Lote
UsuarioCap=row[49], # C50 - UsuarioCap
UsuarioAcr=row[50], # C51 - UsuarioAct
Transportista=row[51], # C52 - Transportista
NumCaja=row[52], # C53 - Transporte + NumTrasporte
Pedimento18=row[53], # C54 - Pedimento18
AduanaCru=row[32], # C33 - Aduana_Cruce
Lote=row[54] # C55 - Lote
)
movements.append(movement)
@@ -334,7 +361,6 @@ class ExportRepairService:
where_conditions.append("ih.operation_type = 'exp'")
# GOLDEN RULE: If movement_type is ALL, only filter by operation_type
if filters.movement_type.value == "ALL":
# ALL mode: bring all exports without filtering by specific invoice_type
pass
else:
where_conditions.append("ih.invoice_type = 'REPAR'")
@@ -345,9 +371,6 @@ class ExportRepairService:
else:
where_conditions.append(f"log.payment_date >= TO_DATE('{filters.start_date}', 'YYYYMMDD') AND log.payment_date <= TO_DATE('{filters.end_date}', 'YYYYMMDD')")
# Note: Status filter applied at Python level after CASE WHEN in SELECT
# because is_updated doesn't directly represent AC/NA status
# Provider filter
if filters.provider:
where_conditions.append(f"cmp.provider_id IN (SELECT id FROM a76.clients_and_providers WHERE name = '{filters.provider}')")
@@ -397,4 +420,4 @@ class ExportRepairService:
def _get_driver_badge(self, db: Session, db_name: str, factura: str) -> str:
"""Get driver badge number for invoice."""
# TODO: GConductor table not migrated to PostgreSQL yet
return None
return None

View File

@@ -29,7 +29,11 @@ class TemporaryImportQueries:
COALESCE(cmp.customs_broker_id::text, '') AS C18,
COALESCE(cmp.aduana, '') AS C38,
ih.id AS C39,
COALESCE(ped_r1.pedimento_number, '') AS C41,
COALESCE(
ped_r1.year || '-' || ped_r1.customs_office || '-' || ped_r1.license || '-' || ped_r1.pedimento_number,
pro_origin.original_pedimento_year || '-' || pro_origin.original_customs_office || '-' || pro_origin.original_license || '-' || pro_origin.original_pedimento_number,
''
) AS C41,
COALESCE(cmp.edocument, '') AS C42,
COALESCE(cmp.vucem_operation_num, '') AS C43,
COALESCE(fin.exchange_rate, 0) AS C50,
@@ -42,18 +46,38 @@ class TemporaryImportQueries:
'' AS C57,
'' AS C58,
COALESCE(fin.value_me, 0) AS total_me,
COALESCE(fin.value_mn, 0) AS total_mn
COALESCE(fin.value_mn, 0) AS total_mn,
COALESCE(lf_agg.sum_value_mxn, 0) AS valor_comercial_mn,
COALESCE(lf_agg.sum_value_temp_mxn, 0) AS valor_mp_temp_mn,
COALESCE(lf_agg.sum_value_added_mxn, 0) AS valor_agre_mn
FROM a76.invoice_header ih
LEFT JOIN a76.invoice_compliance_mx cmp ON cmp.invoice_id = ih.id
LEFT JOIN a76.invoice_financials fin ON fin.invoice_id = ih.id
LEFT JOIN a76.invoice_logistics log ON log.invoice_id = ih.id
LEFT JOIN a76.pedimentos ped ON ped.id = cmp.pedimento_id
LEFT JOIN a76.pedimentos ped_r1 ON ped_r1.id = cmp.pedimento_r1
LEFT JOIN a76.pedimento_rectification_origin pro_origin ON pro_origin.pedimento_id = ped.id
LEFT JOIN a76.pedimento_rectification_origin pro_rect ON
pro_rect.original_pedimento_year = ped.year AND
pro_rect.original_customs_office = ped.customs_office AND
pro_rect.original_license = ped.license AND
pro_rect.original_pedimento_number = ped.pedimento_number
LEFT JOIN a76.pedimentos ped_r1 ON ped_r1.id = pro_rect.pedimento_id
LEFT JOIN (
SELECT i.invoice_id,
SUM(COALESCE(lf.value_mxn, 0)) AS sum_value_mxn,
SUM(COALESCE(lf.value_temp_material_mxn, 0)) AS sum_value_temp_mxn,
SUM(COALESCE(lf.value_added_mxn, 0)) AS sum_value_added_mxn
FROM a76.items i
JOIN a76.item_lines il ON il.item_id = i.id
JOIN a76.item_line_financials lf ON lf.item_line_id = il.id
GROUP BY i.invoice_id
) lf_agg ON lf_agg.invoice_id = ih.id
WHERE ih.operation_type = 'imp'
AND ih.invoice_type = 'TEM'
AND {where_str}
ORDER BY ih.invoice_number
"""
@staticmethod
def build_main_query(db_name: str, where_str: str) -> str:
@@ -101,9 +125,13 @@ class TemporaryImportQueries:
COALESCE(cmp.aduana, '') AS C38,
ih.id AS C39,
FALSE AS C40,
'' AS C41,
COALESCE(cmp.edocument, '') AS C42,
COALESCE(cmp.vucem_operation_num, '') AS C43,
COALESCE(
ped_r1.year || '-' || ped_r1.customs_office || '-' || ped_r1.license || '-' || ped_r1.pedimento_number,
pro_origin.original_pedimento_year || '-' || pro_origin.original_customs_office || '-' || pro_origin.original_license || '-' || pro_origin.original_pedimento_number,
''
) AS C41, -- [40] rectification_id
cmp.edocument AS C42, -- [41]
cmp.vucem_operation_num AS C43, -- [42]
COALESCE(il.line_number, 0) AS C44,
'' AS C45,
'' AS C46,
@@ -124,6 +152,13 @@ class TemporaryImportQueries:
LEFT JOIN a76.invoice_financials fin ON fin.invoice_id = ih.id
LEFT JOIN a76.invoice_logistics log ON log.invoice_id = ih.id
LEFT JOIN a76.pedimentos ped ON ped.id = cmp.pedimento_id
LEFT JOIN a76.pedimento_rectification_origin pro_origin ON pro_origin.pedimento_id = ped.id
LEFT JOIN a76.pedimento_rectification_origin pro_rect ON
pro_rect.original_pedimento_year = ped.year AND
pro_rect.original_customs_office = ped.customs_office AND
pro_rect.original_license = ped.license AND
pro_rect.original_pedimento_number = ped.pedimento_number
LEFT JOIN a76.pedimentos ped_r1 ON ped_r1.id = pro_rect.pedimento_id
LEFT JOIN a76.clients_and_providers prov ON prov.id = cmp.provider_id
LEFT JOIN a76.clients_and_providers client ON client.id = cmp.sold_to_id
LEFT JOIN a76.items i ON i.invoice_id = ih.id
@@ -201,7 +236,11 @@ class DefinitiveImportQueries:
COALESCE(ih.purchase_order, '') AS C31,
COALESCE(cmp.aduana, '') AS C39,
ih.id AS C35,
COALESCE(ped_r1.pedimento_number, '') AS C42,
COALESCE(
ped_r1.year || '-' || ped_r1.customs_office || '-' || ped_r1.license || '-' || ped_r1.pedimento_number,
pro_origin.original_pedimento_year || '-' || pro_origin.original_customs_office || '-' || pro_origin.original_license || '-' || pro_origin.original_pedimento_number,
''
) AS C42,
COALESCE(cmp.edocument, '') AS C43,
COALESCE(cmp.vucem_operation_num, '') AS C44,
COALESCE(fin.exchange_rate, 0) AS C51,
@@ -219,7 +258,13 @@ class DefinitiveImportQueries:
LEFT JOIN a76.invoice_financials fin ON fin.invoice_id = ih.id
LEFT JOIN a76.invoice_logistics log ON log.invoice_id = ih.id
LEFT JOIN a76.pedimentos ped ON ped.id = cmp.pedimento_id
LEFT JOIN a76.pedimentos ped_r1 ON ped_r1.id = cmp.pedimento_r1
LEFT JOIN a76.pedimento_rectification_origin pro_origin ON pro_origin.pedimento_id = ped.id
LEFT JOIN a76.pedimento_rectification_origin pro_rect ON
pro_rect.original_pedimento_year = ped.year AND
pro_rect.original_customs_office = ped.customs_office AND
pro_rect.original_license = ped.license AND
pro_rect.original_pedimento_number = ped.pedimento_number
LEFT JOIN a76.pedimentos ped_r1 ON ped_r1.id = pro_rect.pedimento_id
WHERE ih.operation_type = 'imp'
AND ih.invoice_type IN ('DEF', 'EXDEF', 'MATDE')
AND {where_clause}
@@ -269,7 +314,11 @@ class DefinitiveImportQueries:
cmp.aduana AS C39, -- [38]
il.material_type AS C40, -- [39]
il.id AS C41, -- [40]
'' AS C42, -- [41] rectification_id
COALESCE(
ped_r1.year || '-' || ped_r1.customs_office || '-' || ped_r1.license || '-' || ped_r1.pedimento_number,
pro_origin.original_pedimento_year || '-' || pro_origin.original_customs_office || '-' || pro_origin.original_license || '-' || pro_origin.original_pedimento_number,
''
) AS C42, -- [41] rectification_id
cmp.edocument AS C43, -- [42]
cmp.vucem_operation_num AS C44, -- [43]
il.line_number AS C45, -- [44]
@@ -293,6 +342,13 @@ class DefinitiveImportQueries:
LEFT JOIN a76.invoice_financials fin ON fin.invoice_id = ih.id
LEFT JOIN a76.invoice_logistics log ON log.invoice_id = ih.id
LEFT JOIN a76.pedimentos ped ON ped.id = cmp.pedimento_id
LEFT JOIN a76.pedimento_rectification_origin pro_origin ON pro_origin.pedimento_id = ped.id
LEFT JOIN a76.pedimento_rectification_origin pro_rect ON
pro_rect.original_pedimento_year = ped.year AND
pro_rect.original_customs_office = ped.customs_office AND
pro_rect.original_license = ped.license AND
pro_rect.original_pedimento_number = ped.pedimento_number
LEFT JOIN a76.pedimentos ped_r1 ON ped_r1.id = pro_rect.pedimento_id
LEFT JOIN a76.clients_and_providers prov ON prov.id = cmp.provider_id
LEFT JOIN a76.clients_and_providers client ON client.id = cmp.sold_to_id
LEFT JOIN a76.items itm ON itm.invoice_id = ih.id
@@ -388,22 +444,34 @@ class RepairImportQueries:
COALESCE(log.transport_num, '') AS C45,
COALESCE(ped.pedimento_code, '') AS C47,
COALESCE(fin.value_me, 0) AS total_me,
COALESCE(fin.value_mn, 0) AS total_mn
COALESCE(fin.value_mn, 0) AS total_mn,
COALESCE(
ped_r1.year || '-' || ped_r1.customs_office || '-' || ped_r1.license || '-' || ped_r1.pedimento_number,
pro_origin.original_pedimento_year || '-' || pro_origin.original_customs_office || '-' || pro_origin.original_license || '-' || pro_origin.original_pedimento_number,
''
) AS C48
FROM a76.invoice_header ih
LEFT JOIN a76.invoice_compliance_mx cmp ON cmp.invoice_id = ih.id
LEFT JOIN a76.invoice_financials fin ON fin.invoice_id = ih.id
LEFT JOIN a76.invoice_logistics log ON log.invoice_id = ih.id
LEFT JOIN a76.pedimentos ped ON ped.id = cmp.pedimento_id
LEFT JOIN a76.items i ON i.invoice_id = ih.id
LEFT JOIN a76.pedimento_rectification_origin pro_origin ON pro_origin.pedimento_id = ped.id
LEFT JOIN a76.pedimento_rectification_origin pro_rect ON
pro_rect.original_pedimento_year = ped.year AND
pro_rect.original_customs_office = ped.customs_office AND
pro_rect.original_license = ped.license AND
pro_rect.original_pedimento_number = ped.pedimento_number
LEFT JOIN a76.pedimentos ped_r1 ON ped_r1.id = pro_rect.pedimento_id
WHERE ih.operation_type = 'imp'
AND COALESCE(cmp.is_regime_change, false) = false
AND EXISTS (
SELECT 1 FROM a76.item_lines il2
SELECT 1 FROM a76.items i2
INNER JOIN a76.item_lines il2 ON il2.item_id = i2.id
INNER JOIN a24.fa_item_lines fil2 ON fil2.id = il2.id
WHERE il2.item_id = i.id AND fil2.search_invoice IS NOT NULL
WHERE i2.invoice_id = ih.id AND fil2.search_invoice IS NOT NULL
{discharge_filter}
)
{"AND " + where_str if where_str else ""}
{discharge_filter}
ORDER BY ih.invoice_number
"""
@@ -450,7 +518,11 @@ class RepairImportQueries:
COALESCE(ped.customs_office, ''),
ih.id,
'P',
'',
COALESCE(
ped_r1.year || '-' || ped_r1.customs_office || '-' || ped_r1.license || '-' || ped_r1.pedimento_number,
pro_origin.original_pedimento_year || '-' || pro_origin.original_customs_office || '-' || pro_origin.original_license || '-' || pro_origin.original_pedimento_number,
''
),
COALESCE(cmp.edocument, ''),
COALESCE(cmp.vucem_operation_num, ''),
REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(ld.brand, ''), CHR(44), ' '), CHR(9), ' '), CHR(10), ' '), CHR(13), ' '),
@@ -470,6 +542,13 @@ class RepairImportQueries:
LEFT JOIN a76.invoice_financials fin ON fin.invoice_id = ih.id
LEFT JOIN a76.invoice_logistics log ON log.invoice_id = ih.id
LEFT JOIN a76.pedimentos ped ON ped.id = cmp.pedimento_id
LEFT JOIN a76.pedimento_rectification_origin pro_origin ON pro_origin.pedimento_id = ped.id
LEFT JOIN a76.pedimento_rectification_origin pro_rect ON
pro_rect.original_pedimento_year = ped.year AND
pro_rect.original_customs_office = ped.customs_office AND
pro_rect.original_license = ped.license AND
pro_rect.original_pedimento_number = ped.pedimento_number
LEFT JOIN a76.pedimentos ped_r1 ON ped_r1.id = pro_rect.pedimento_id
LEFT JOIN a76.items itm ON itm.invoice_id = ih.id
LEFT JOIN a76.item_lines il ON il.item_id = itm.id
LEFT JOIN a76.item_line_descriptions ld ON ld.item_line_id = il.id
@@ -568,12 +647,24 @@ class ExportQueries:
COALESCE(ih.who_updated, '') AS C51,
COALESCE(log.transport_id || ' ' || log.transport_num, '') AS C53,
COALESCE(fin.value_me, 0) AS total_me,
COALESCE(fin.value_mn, 0) AS total_mn
COALESCE(fin.value_mn, 0) AS total_mn,
COALESCE(
ped_r1.year || '-' || ped_r1.customs_office || '-' || ped_r1.license || '-' || ped_r1.pedimento_number,
pro_origin.original_pedimento_year || '-' || pro_origin.original_customs_office || '-' || pro_origin.original_license || '-' || pro_origin.original_pedimento_number,
''
) AS C54
FROM a76.invoice_header ih
LEFT JOIN a76.invoice_compliance_mx cmp ON cmp.invoice_id = ih.id
LEFT JOIN a76.invoice_financials fin ON fin.invoice_id = ih.id
LEFT JOIN a76.invoice_logistics log ON log.invoice_id = ih.id
LEFT JOIN a76.pedimentos ped ON ped.id = cmp.pedimento_id
LEFT JOIN a76.pedimento_rectification_origin pro_origin ON pro_origin.pedimento_id = ped.id
LEFT JOIN a76.pedimento_rectification_origin pro_rect ON
pro_rect.original_pedimento_year = ped.year AND
pro_rect.original_customs_office = ped.customs_office AND
pro_rect.original_license = ped.license AND
pro_rect.original_pedimento_number = ped.pedimento_number
LEFT JOIN a76.pedimentos ped_r1 ON ped_r1.id = pro_rect.pedimento_id
WHERE {where_clause}
ORDER BY ih.invoice_number
"""
@@ -620,7 +711,11 @@ class ExportQueries:
lf.value_mxn AS C36, -- [35]
lf.value_usd AS C37, -- [36]
il.material_type AS C38, -- [37]
'' AS C39, -- [38] rectification_id
COALESCE(
ped_r1.year || '-' || ped_r1.customs_office || '-' || ped_r1.license || '-' || ped_r1.pedimento_number,
pro_origin.original_pedimento_year || '-' || pro_origin.original_customs_office || '-' || pro_origin.original_license || '-' || pro_origin.original_pedimento_number,
''
) AS C39, -- [38] rectification_id
cmp.edocument AS C40, -- [39]
cmp.vucem_operation_num AS C41, -- [40]
il.line_number AS C42, -- [41]
@@ -647,6 +742,13 @@ class ExportQueries:
LEFT JOIN a76.invoice_financials fin ON fin.invoice_id = ih.id
LEFT JOIN a76.invoice_logistics log ON log.invoice_id = ih.id
LEFT JOIN a76.pedimentos ped ON ped.id = cmp.pedimento_id
LEFT JOIN a76.pedimento_rectification_origin pro_origin ON pro_origin.pedimento_id = ped.id
LEFT JOIN a76.pedimento_rectification_origin pro_rect ON
pro_rect.original_pedimento_year = ped.year AND
pro_rect.original_customs_office = ped.customs_office AND
pro_rect.original_license = ped.license AND
pro_rect.original_pedimento_number = ped.pedimento_number
LEFT JOIN a76.pedimentos ped_r1 ON ped_r1.id = pro_rect.pedimento_id
LEFT JOIN a76.items itm ON itm.invoice_id = ih.id
LEFT JOIN a76.item_lines il ON il.item_id = itm.id
LEFT JOIN a76.item_line_descriptions ld ON ld.item_line_id = il.id
@@ -745,12 +847,24 @@ class ExportRepairQueries:
COALESCE(log.carrier_id, '') AS C52,
COALESCE(log.transport_id || ' ' || log.transport_num, '') AS C53,
COALESCE(fin.value_me, 0) AS total_me,
COALESCE(fin.value_mn, 0) AS total_mn
COALESCE(fin.value_mn, 0) AS total_mn,
COALESCE(
ped_r1.year || '-' || ped_r1.customs_office || '-' || ped_r1.license || '-' || ped_r1.pedimento_number,
pro_origin.original_pedimento_year || '-' || pro_origin.original_customs_office || '-' || pro_origin.original_license || '-' || pro_origin.original_pedimento_number,
''
) AS C54
FROM a76.invoice_header ih
LEFT JOIN a76.invoice_compliance_mx cmp ON cmp.invoice_id = ih.id
LEFT JOIN a76.invoice_financials fin ON fin.invoice_id = ih.id
LEFT JOIN a76.invoice_logistics log ON log.invoice_id = ih.id
LEFT JOIN a76.pedimentos ped ON ped.id = cmp.pedimento_id
LEFT JOIN a76.pedimento_rectification_origin pro_origin ON pro_origin.pedimento_id = ped.id
LEFT JOIN a76.pedimento_rectification_origin pro_rect ON
pro_rect.original_pedimento_year = ped.year AND
pro_rect.original_customs_office = ped.customs_office AND
pro_rect.original_license = ped.license AND
pro_rect.original_pedimento_number = ped.pedimento_number
LEFT JOIN a76.pedimentos ped_r1 ON ped_r1.id = pro_rect.pedimento_id
WHERE ih.operation_type = 'exp'
AND ih.invoice_type = 'REPAR'
{"AND " + where_str if where_str else ""}
@@ -800,7 +914,11 @@ class ExportRepairQueries:
COALESCE(lf.value_mxn, 0) AS C36,
COALESCE(lf.value_usd, 0) AS C37,
'P' AS C38,
'' AS C39,
COALESCE(
ped_r1.year || '-' || ped_r1.customs_office || '-' || ped_r1.license || '-' || ped_r1.pedimento_number,
pro_origin.original_pedimento_year || '-' || pro_origin.original_customs_office || '-' || pro_origin.original_license || '-' || pro_origin.original_pedimento_number,
''
) AS C39,
COALESCE(cmp.edocument, '') AS C40,
COALESCE(cmp.vucem_operation_num, '') AS C41,
COALESCE(il.line_number, 0) AS C42,
@@ -826,6 +944,13 @@ class ExportRepairQueries:
LEFT JOIN a76.invoice_financials fin ON fin.invoice_id = ih.id
LEFT JOIN a76.invoice_logistics log ON log.invoice_id = ih.id
LEFT JOIN a76.pedimentos ped ON ped.id = cmp.pedimento_id
LEFT JOIN a76.pedimento_rectification_origin pro_origin ON pro_origin.pedimento_id = ped.id
LEFT JOIN a76.pedimento_rectification_origin pro_rect ON
pro_rect.original_pedimento_year = ped.year AND
pro_rect.original_customs_office = ped.customs_office AND
pro_rect.original_license = ped.license AND
pro_rect.original_pedimento_number = ped.pedimento_number
LEFT JOIN a76.pedimentos ped_r1 ON ped_r1.id = pro_rect.pedimento_id
LEFT JOIN a76.items itm ON itm.invoice_id = ih.id
LEFT JOIN a76.item_lines il ON il.item_id = itm.id
LEFT JOIN a76.item_line_descriptions ld ON ld.item_line_id = il.id

View File

@@ -114,8 +114,8 @@ class RepairImportService:
# Get pedimento rectification
pedimento_r1 = DatabaseHelper.get_rectification_pedimento(
db,
row[1], # C3 - PedimentoImpoRep
'', # PedRectifica not in aggregated query
row[1], # C3 - PedimentoImpoRep
row[26], # C48 - PedRectifica
filters.is_shelter
)
@@ -258,7 +258,7 @@ class RepairImportService:
)
pedimento_r1 = DatabaseHelper.get_rectification_pedimento(
db, row[2], row[41], filters.is_shelter
db, row[2], row[32], filters.is_shelter
)
num_gaf_uni = DatabaseHelper.get_driver_badge(

View File

@@ -94,6 +94,9 @@ class TemporaryImportService:
# Correct indices based on TemporaryImportQueries.build_aggregated_query
total_me = to_float(row[28]) # total_me (index 28)
total_mn = to_float(row[29]) # total_mn (index 29)
valor_comercial_mn = to_float(row[30]) # valor_comercial_mn from item_line_financials
valor_mp_temp_mn = to_float(row[31]) # valor_mp_temp_mn from item_line_financials
valor_agre_mn = to_float(row[32]) # valor_agre_mn from item_line_financials
# Calculate exchange rate and value
valor_comercial, tipo_cambio = ExchangeRateCalculator.calculate_for_aggregated(
@@ -134,10 +137,10 @@ class TemporaryImportService:
ClavePed=row[4], # C5 - ClavePed
TipoMovTemDef='IMTEM',
EsCambioRegimen='N',
ValorMPTemp=valor_comercial,
ValorComercialMN=valor_comercial,
ValorMPTemp=valor_mp_temp_mn,
ValorComercialMN=valor_comercial_mn,
TipoCambio=tipo_cambio,
ValorAgre=0.0,
ValorAgre=valor_agre_mn,
TipoExpo='',
PedimentoR1=pedimento_r1,
EDocument=row[17], # C42 - EDocument
@@ -147,10 +150,10 @@ class TemporaryImportService:
UsuarioCap=row[21], # C52 - UsuarioCap
UsuarioAcr=row[22], # C53 - UsuarioAct
Fecha_Pago=parse_yyyymmdd_date(none_if_empty(row[8])), # C13 - Fecha_Pago
NumCaja=row[23], # C55 - Transporte + NumTrasporte
Pedimento18=row[24], # C56 - Pedimento18 (Actually empty in query, but safe to keep)
AduanaCru=row[14], # C38 - Aduana_Cruce
Lote=row[25] # C57 - LOTE (Actually C55 is index 24. C56 is 25)
NumCaja=row[24], # C55 - transport_num || license_plate (index 24)
Pedimento18=row[25], # C56 - '' empty (index 25)
AduanaCru=row[14], # C38 - Aduana_Cruce (index 14)
Lote=row[26] # C57 - '' empty (index 26)
)
movements.append(movement)