diff --git a/backend/alembic/versions/ca7d3c4e8b2a_invoice_logistics_int_fk_constraints.py b/backend/alembic/versions/ca7d3c4e8b2a_invoice_logistics_int_fk_constraints.py new file mode 100644 index 00000000..394da688 --- /dev/null +++ b/backend/alembic/versions/ca7d3c4e8b2a_invoice_logistics_int_fk_constraints.py @@ -0,0 +1,266 @@ +"""Add surrogate int IDs for transport catalogs and invoice logistics. + +Single consolidated migration for: +- transporter_id, trailer_id, vehicle_id, driver_id +- carrier_int_id, transport_int_id, trailer_int_id in invoice_logistics +- backfill mappings from existing string keys/codes +- FK constraints for the new invoice_logistics int references +""" + +from __future__ import annotations + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +revision: str = "ca7d3c4e8b2a" +down_revision: Union[str, Sequence[str], None] = "c1a2b3d4e5f6" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # transporter + op.add_column("transporter", sa.Column("transporter_id", sa.BigInteger(), nullable=True), schema="a76") + op.execute( + """ + WITH s AS ( + SELECT transporter_key, ROW_NUMBER() OVER (ORDER BY transporter_key) AS new_id + FROM a76.transporter + ) + UPDATE a76.transporter t + SET transporter_id = s.new_id + FROM s + WHERE t.transporter_key = s.transporter_key + """ + ) + op.execute("CREATE SEQUENCE IF NOT EXISTS a76.transporter_transporter_id_seq") + op.execute( + """ + SELECT setval( + 'a76.transporter_transporter_id_seq', + COALESCE((SELECT MAX(transporter_id) FROM a76.transporter), 0) + 1, + false + ) + """ + ) + op.execute( + """ + ALTER TABLE a76.transporter + ALTER COLUMN transporter_id + SET DEFAULT nextval('a76.transporter_transporter_id_seq') + """ + ) + op.execute("ALTER TABLE a76.transporter ALTER COLUMN transporter_id SET NOT NULL") + op.create_unique_constraint("uq_a76_transporter_transporter_id", "transporter", ["transporter_id"], schema="a76") + + # trailer + op.add_column("trailer", sa.Column("trailer_id", sa.BigInteger(), nullable=True), schema="a76") + op.execute( + """ + WITH s AS ( + SELECT trailer_number, ROW_NUMBER() OVER (ORDER BY trailer_number) AS new_id + FROM a76.trailer + ) + UPDATE a76.trailer t + SET trailer_id = s.new_id + FROM s + WHERE t.trailer_number = s.trailer_number + """ + ) + op.execute("CREATE SEQUENCE IF NOT EXISTS a76.trailer_trailer_id_seq") + op.execute( + """ + SELECT setval( + 'a76.trailer_trailer_id_seq', + COALESCE((SELECT MAX(trailer_id) FROM a76.trailer), 0) + 1, + false + ) + """ + ) + op.execute( + """ + ALTER TABLE a76.trailer + ALTER COLUMN trailer_id + SET DEFAULT nextval('a76.trailer_trailer_id_seq') + """ + ) + op.execute("ALTER TABLE a76.trailer ALTER COLUMN trailer_id SET NOT NULL") + op.create_unique_constraint("uq_a76_trailer_trailer_id", "trailer", ["trailer_id"], schema="a76") + + # vehicle + op.add_column("vehicle", sa.Column("vehicle_id", sa.BigInteger(), nullable=True), schema="a76") + op.execute( + """ + WITH s AS ( + SELECT vehicle_key, ROW_NUMBER() OVER (ORDER BY vehicle_key) AS new_id + FROM a76.vehicle + ) + UPDATE a76.vehicle v + SET vehicle_id = s.new_id + FROM s + WHERE v.vehicle_key = s.vehicle_key + """ + ) + op.execute("CREATE SEQUENCE IF NOT EXISTS a76.vehicle_vehicle_id_seq") + op.execute( + """ + SELECT setval( + 'a76.vehicle_vehicle_id_seq', + COALESCE((SELECT MAX(vehicle_id) FROM a76.vehicle), 0) + 1, + false + ) + """ + ) + op.execute( + """ + ALTER TABLE a76.vehicle + ALTER COLUMN vehicle_id + SET DEFAULT nextval('a76.vehicle_vehicle_id_seq') + """ + ) + op.execute("ALTER TABLE a76.vehicle ALTER COLUMN vehicle_id SET NOT NULL") + op.create_unique_constraint("uq_a76_vehicle_vehicle_id", "vehicle", ["vehicle_id"], schema="a76") + + # driver + op.add_column("driver", sa.Column("driver_id", sa.BigInteger(), nullable=True), schema="a76") + op.execute( + """ + WITH s AS ( + SELECT transporter_key, line, ROW_NUMBER() OVER (ORDER BY transporter_key, line) AS new_id + FROM a76.driver + ) + UPDATE a76.driver d + SET driver_id = s.new_id + FROM s + WHERE d.transporter_key = s.transporter_key + AND d.line = s.line + """ + ) + op.execute("CREATE SEQUENCE IF NOT EXISTS a76.driver_driver_id_seq") + op.execute( + """ + SELECT setval( + 'a76.driver_driver_id_seq', + COALESCE((SELECT MAX(driver_id) FROM a76.driver), 0) + 1, + false + ) + """ + ) + op.execute( + """ + ALTER TABLE a76.driver + ALTER COLUMN driver_id + SET DEFAULT nextval('a76.driver_driver_id_seq') + """ + ) + op.execute("ALTER TABLE a76.driver ALTER COLUMN driver_id SET NOT NULL") + op.create_unique_constraint("uq_a76_driver_driver_id", "driver", ["driver_id"], schema="a76") + + # invoice_logistics int refs + op.add_column("invoice_logistics", sa.Column("carrier_int_id", sa.BigInteger(), nullable=True), schema="a76") + op.add_column("invoice_logistics", sa.Column("transport_int_id", sa.BigInteger(), nullable=True), schema="a76") + op.add_column("invoice_logistics", sa.Column("trailer_int_id", sa.BigInteger(), nullable=True), schema="a76") + + # ensure referenced codes exist + op.execute( + """ + INSERT INTO a76.transporter (transporter_key, tenant_id, company_id) + SELECT DISTINCT ON (il.carrier_id) il.carrier_id, il.tenant_id, il.company_id + FROM a76.invoice_logistics il + WHERE il.carrier_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM a76.transporter t WHERE t.transporter_key = il.carrier_id + ) + """ + ) + op.execute( + """ + INSERT INTO a76.vehicle (vehicle_key, tenant_id, company_id) + SELECT DISTINCT ON (il.transport_id) il.transport_id, il.tenant_id, il.company_id + FROM a76.invoice_logistics il + WHERE il.transport_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM a76.vehicle v WHERE v.vehicle_key = il.transport_id + ) + """ + ) + op.execute( + """ + INSERT INTO a76.trailer (trailer_number, tenant_id, company_id) + SELECT DISTINCT ON (il.trailer_num) il.trailer_num, il.tenant_id, il.company_id + FROM a76.invoice_logistics il + WHERE il.trailer_num IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM a76.trailer tr WHERE tr.trailer_number = il.trailer_num + ) + """ + ) + + # backfill int refs + op.execute( + """ + UPDATE a76.invoice_logistics il + SET carrier_int_id = t.transporter_id + FROM a76.transporter t + WHERE il.carrier_id IS NOT NULL + AND il.carrier_id = t.transporter_key + """ + ) + op.execute( + """ + UPDATE a76.invoice_logistics il + SET transport_int_id = v.vehicle_id + FROM a76.vehicle v + WHERE il.transport_id IS NOT NULL + AND il.transport_id = v.vehicle_key + """ + ) + op.execute( + """ + UPDATE a76.invoice_logistics il + SET trailer_int_id = tr.trailer_id + FROM a76.trailer tr + WHERE il.trailer_num IS NOT NULL + AND il.trailer_num = tr.trailer_number + """ + ) + + # FK constraints + op.create_foreign_key( + "fk_a76_invoice_logistics_carrier_int_id_transporter_id", + "invoice_logistics", + "transporter", + ["carrier_int_id"], + ["transporter_id"], + source_schema="a76", + referent_schema="a76", + ondelete="SET NULL", + ) + op.create_foreign_key( + "fk_a76_invoice_logistics_transport_int_id_vehicle_id", + "invoice_logistics", + "vehicle", + ["transport_int_id"], + ["vehicle_id"], + source_schema="a76", + referent_schema="a76", + ondelete="SET NULL", + ) + op.create_foreign_key( + "fk_a76_invoice_logistics_trailer_int_id_trailer_id", + "invoice_logistics", + "trailer", + ["trailer_int_id"], + ["trailer_id"], + source_schema="a76", + referent_schema="a76", + ondelete="SET NULL", + ) + + +def downgrade() -> None: + pass + diff --git a/backend/api/v1/modules/a76/invoices/common/common_validators.py b/backend/api/v1/modules/a76/invoices/common/common_validators.py index 3f7dc48b..558f6940 100644 --- a/backend/api/v1/modules/a76/invoices/common/common_validators.py +++ b/backend/api/v1/modules/a76/invoices/common/common_validators.py @@ -512,15 +512,29 @@ def validate_common( if invoice.logistics: if invoice.logistics.carrier_id: - carrier_exists = ( - db.query(Transporter) - .filter( - Transporter.id == invoice.logistics.carrier_id, - Transporter.tenant_id == tenant_id, - Transporter.company_id == company_id, + # `carrier_id` is the frontend/export key (string). Prefer validating by the internal int + # when present, otherwise validate by `transporter_key`. + if getattr(invoice.logistics, "carrier_int_id", None) is not None: + carrier_exists = ( + db.query(Transporter) + .filter( + Transporter.transporter_id + == invoice.logistics.carrier_int_id, + Transporter.tenant_id == tenant_id, + Transporter.company_id == company_id, + ) + .first() + ) + else: + carrier_exists = ( + db.query(Transporter) + .filter( + Transporter.transporter_key == invoice.logistics.carrier_id, + Transporter.tenant_id == tenant_id, + Transporter.company_id == company_id, + ) + .first() ) - .first() - ) if not carrier_exists: errors.add_error( field="logistics.carrier_id", diff --git a/backend/api/v1/modules/a76/invoices/models.py b/backend/api/v1/modules/a76/invoices/models.py index a10be8c7..f6723b3e 100644 --- a/backend/api/v1/modules/a76/invoices/models.py +++ b/backend/api/v1/modules/a76/invoices/models.py @@ -563,9 +563,15 @@ class InvoiceLogistics(Base, TenantScopedMixin, TimestampMixin): carrier_id: Mapped[Optional[str]] = mapped_column( String(10) ) # TRANSPORTISTA / Transportista + carrier_int_id: Mapped[Optional[int]] = mapped_column( + BigInteger, ForeignKey("a76.transporter.transporter_id"), nullable=True + ) # Internal surrogate ID for Transporter (by transporter_key) transport_id: Mapped[Optional[str]] = mapped_column( String(10) ) # NUMTRAILER / Transportista + transport_int_id: Mapped[Optional[int]] = mapped_column( + BigInteger, ForeignKey("a76.vehicle.vehicle_id"), nullable=True + ) # Internal surrogate ID for Vehicle (by vehicle_key) transport_us_id: Mapped[Optional[str]] = mapped_column( String(10) ) # TRANSPORTISTAAME / Transportista americano @@ -601,6 +607,9 @@ class InvoiceLogistics(Base, TenantScopedMixin, TimestampMixin): trailer_num: Mapped[Optional[str]] = mapped_column( String(20) ) # NUMTRAILER / NĂºmero de trailer + trailer_int_id: Mapped[Optional[int]] = mapped_column( + BigInteger, ForeignKey("a76.trailer.trailer_id"), nullable=True + ) # Internal surrogate ID for Trailer (by trailer_number) seal_number: Mapped[Optional[str]] = mapped_column( String(15) ) # PRECINTO / Precinto diff --git a/backend/api/v1/modules/a76/invoices/schemas.py b/backend/api/v1/modules/a76/invoices/schemas.py index 46d0997a..e8df1724 100644 --- a/backend/api/v1/modules/a76/invoices/schemas.py +++ b/backend/api/v1/modules/a76/invoices/schemas.py @@ -299,9 +299,14 @@ class InvoiceFinancialsBase(BaseModel): class InvoiceLogisticsBase(BaseModel): """Base fields for Logistics""" - - carrier_id: Optional[str] = Field(None, max_length=10, description="Carrier ID") + carrier_id: Optional[str] = Field(None, max_length=10, description="Carrier code (front-end key)") + carrier_int_id: Optional[int] = Field( + None, description="Internal carrier integer ID (mapped from carrier_id key)" + ) transport_id: Optional[str] = Field(None, max_length=10, description="Transport ID") + transport_int_id: Optional[int] = Field( + None, description="Internal vehicle integer ID (mapped from transport_id key)" + ) transport_us_id: Optional[str] = Field( None, max_length=10, description="US transport ID" ) @@ -329,6 +334,9 @@ class InvoiceLogisticsBase(BaseModel): trailer_num: Optional[str] = Field( None, max_length=20, description="Trailer number" ) + trailer_int_id: Optional[int] = Field( + None, description="Internal trailer integer ID (mapped from trailer_num key)" + ) seal_number: Optional[str] = Field(None, max_length=15, description="Seal number") guide_number: Optional[str] = Field(None, max_length=20, description="Guide number") bill_number: Optional[str] = Field(None, max_length=15, description="Bill number") diff --git a/backend/api/v1/modules/a76/invoices/services.py b/backend/api/v1/modules/a76/invoices/services.py index a9871ad6..5bcd6fd0 100644 --- a/backend/api/v1/modules/a76/invoices/services.py +++ b/backend/api/v1/modules/a76/invoices/services.py @@ -15,6 +15,87 @@ from api.v1.modules.a76.items.models import LineItem from . import models, schemas +def _autofill_transport_int_ids( + db: Session, + logistics_target, + tenant_id: int, + company_id: int, +) -> None: + """ + Ensures invoice logistics internal int IDs are populated when their string codes exist. + + The frontend and exports/imports keep using the string keys/codes (carrier_id, transport_id, trailer_num), + but internally we also persist integer surrogate IDs for fast/consistent joins. + """ + + # Local imports to avoid circular dependencies. + from api.v1.modules.a76.transportation.transporters.models import Transporter + from api.v1.modules.a76.transportation.vehicles.models import Vehicle + from api.v1.modules.a76.transportation.trailers.models import Trailer + + def _get(field: str): + if isinstance(logistics_target, dict): + return logistics_target.get(field) + return getattr(logistics_target, field, None) + + def _set(field: str, value): + if isinstance(logistics_target, dict): + logistics_target[field] = value + else: + setattr(logistics_target, field, value) + + carrier_code = _get("carrier_id") + carrier_int_id = _get("carrier_int_id") + if carrier_code and carrier_int_id is None: + transporter_obj = ( + db.query(Transporter) + .filter(Transporter.transporter_key == carrier_code) + .first() + ) + if not transporter_obj: + # Minimal insert so mapping doesn't end up NULL. + transporter_obj = Transporter( + transporter_key=carrier_code, + tenant_id=tenant_id, + company_id=company_id, + ) + db.add(transporter_obj) + db.flush() + _set("carrier_int_id", transporter_obj.transporter_id) + + transport_code = _get("transport_id") + transport_int_id = _get("transport_int_id") + if transport_code and transport_int_id is None: + vehicle_obj = ( + db.query(Vehicle).filter(Vehicle.vehicle_key == transport_code).first() + ) + if not vehicle_obj: + vehicle_obj = Vehicle( + vehicle_key=transport_code, + tenant_id=tenant_id, + company_id=company_id, + ) + db.add(vehicle_obj) + db.flush() + _set("transport_int_id", vehicle_obj.vehicle_id) + + trailer_code = _get("trailer_num") + trailer_int_id = _get("trailer_int_id") + if trailer_code and trailer_int_id is None: + trailer_obj = ( + db.query(Trailer).filter(Trailer.trailer_number == trailer_code).first() + ) + if not trailer_obj: + trailer_obj = Trailer( + trailer_number=trailer_code, + tenant_id=tenant_id, + company_id=company_id, + ) + db.add(trailer_obj) + db.flush() + _set("trailer_int_id", trailer_obj.trailer_id) + + def _get_current_username() -> str: """Helper to get current username from context or fallback to System""" try: @@ -282,6 +363,12 @@ class InvoiceService: logistics_dict["invoice_id"] = new_invoice.id logistics_dict["tenant_id"] = tenant_id logistics_dict["company_id"] = company_id + _autofill_transport_int_ids( + db, + logistics_dict, + tenant_id=tenant_id, + company_id=company_id, + ) new_logistics = models.InvoiceLogistics(**logistics_dict) db.add(new_logistics) @@ -444,11 +531,23 @@ class InvoiceService: if value == "": value = None setattr(invoice.logistics, key, value) + _autofill_transport_int_ids( + db, + invoice.logistics, + tenant_id=tenant_id, + company_id=company_id, + ) 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 + _autofill_transport_int_ids( + db, + logistics_dict, + tenant_id=tenant_id, + company_id=company_id, + ) new_logistics = models.InvoiceLogistics(**logistics_dict) db.add(new_logistics) diff --git a/backend/api/v1/modules/a76/layouts_csv/facturas/tasks.py b/backend/api/v1/modules/a76/layouts_csv/facturas/tasks.py index f1ad7d80..7717a874 100644 --- a/backend/api/v1/modules/a76/layouts_csv/facturas/tasks.py +++ b/backend/api/v1/modules/a76/layouts_csv/facturas/tasks.py @@ -6071,8 +6071,30 @@ def _do_insert_valid_rows(job_id: str, model_target: str, job_type_override: Opt transport_type = TransportType(transport_str) except ValueError: transport_type = TransportType.NONE + + carrier_code = row_norm.get('CLAVE TRANSPORTISTA') or None + carrier_int_id = None + if carrier_code: + # Internal int mapping for carrier_code -> transporter_id + from api.v1.modules.a76.transportation.transporters.models import Transporter + + carrier_obj = ( + session.query(Transporter) + .filter(Transporter.transporter_key == carrier_code) + .first() + ) + if not carrier_obj: + carrier_obj = Transporter( + transporter_key=carrier_code, + tenant_id=tenant_id, + company_id=company_id, + ) + session.add(carrier_obj) + session.flush() + carrier_int_id = carrier_obj.transporter_id logistics = InvoiceLogistics( - carrier_id=(row_norm.get('CLAVE TRANSPORTISTA') or None), + carrier_id=carrier_code, + carrier_int_id=carrier_int_id, driver_name=(row_norm.get('NOMBRE CONDUCTOR') or None), transport_type=transport_type, transport_num=(row_norm.get('NUMERO TRANSPORTE') or None), diff --git a/backend/api/v1/modules/a76/reports/importacion/consolidados/mex/service.py b/backend/api/v1/modules/a76/reports/importacion/consolidados/mex/service.py index b35c8bc1..c90b81b1 100644 --- a/backend/api/v1/modules/a76/reports/importacion/consolidados/mex/service.py +++ b/backend/api/v1/modules/a76/reports/importacion/consolidados/mex/service.py @@ -298,7 +298,13 @@ class ConsolidadoImportacionMexService: if logistics: # 1. Transporter (CAAT / SCAC) - if logistics.carrier_id: + if logistics.carrier_int_id is not None: + transporter_obj = ( + db.query(Transporter) + .filter(Transporter.transporter_id == logistics.carrier_int_id) + .first() + ) + elif logistics.carrier_id: transporter_obj = ( db.query(Transporter) .filter(Transporter.transporter_key == logistics.carrier_id) @@ -342,11 +348,18 @@ class ConsolidadoImportacionMexService: # 2. Vehicle (Placas Tracto) - Try transport_id first if logistics.transport_id: - veh_obj = ( - db.query(Vehicle) - .filter(Vehicle.vehicle_key == logistics.transport_id) - .first() - ) + if logistics.transport_int_id is not None: + veh_obj = ( + db.query(Vehicle) + .filter(Vehicle.vehicle_id == logistics.transport_int_id) + .first() + ) + else: + veh_obj = ( + db.query(Vehicle) + .filter(Vehicle.vehicle_key == logistics.transport_id) + .first() + ) if veh_obj: placas_val = veh_obj.plate_number or placas_val elif ( @@ -362,11 +375,18 @@ class ConsolidadoImportacionMexService: # 3. Trailer (Placas Remolque) if logistics.trailer_num: - trl_obj = ( - db.query(Trailer) - .filter(Trailer.trailer_number == logistics.trailer_num) - .first() - ) + if logistics.trailer_int_id is not None: + trl_obj = ( + db.query(Trailer) + .filter(Trailer.trailer_id == logistics.trailer_int_id) + .first() + ) + else: + trl_obj = ( + db.query(Trailer) + .filter(Trailer.trailer_number == logistics.trailer_num) + .first() + ) if trl_obj: placas_remolque_val = trl_obj.plate_number or "" diff --git a/backend/api/v1/modules/a76/reports/importacion/consolidados/temporary/mex/service.py b/backend/api/v1/modules/a76/reports/importacion/consolidados/temporary/mex/service.py index a3949d07..d942d1ce 100644 --- a/backend/api/v1/modules/a76/reports/importacion/consolidados/temporary/mex/service.py +++ b/backend/api/v1/modules/a76/reports/importacion/consolidados/temporary/mex/service.py @@ -175,8 +175,16 @@ class ConsolidadoImportacionMexService: if logistics: # 1. Transporter (CAAT / SCAC) - if logistics.carrier_id: - transporter_obj = db.query(Transporter).filter(Transporter.transporter_key == logistics.carrier_id).first() + if logistics.carrier_int_id is not None: + transporter_obj = ( + db.query(Transporter) + .filter(Transporter.transporter_id == logistics.carrier_int_id) + .first() + ) + elif logistics.carrier_id: + transporter_obj = db.query(Transporter).filter( + Transporter.transporter_key == logistics.carrier_id + ).first() if transporter_obj: caat_val = transporter_obj.caat_code or "" scac_val = transporter_obj.transport_code or "" # Mapping transport_code to SCAC @@ -213,7 +221,16 @@ class ConsolidadoImportacionMexService: # 2. Vehicle (Placas Tracto) - Try transport_id first if logistics.transport_id: - veh_obj = db.query(Vehicle).filter(Vehicle.vehicle_key == logistics.transport_id).first() + if logistics.transport_int_id is not None: + veh_obj = ( + db.query(Vehicle) + .filter(Vehicle.vehicle_id == logistics.transport_int_id) + .first() + ) + else: + veh_obj = db.query(Vehicle).filter( + Vehicle.vehicle_key == logistics.transport_id + ).first() if veh_obj: placas_val = veh_obj.plate_number or placas_val elif logistics.vehicle_num: # Fallback to vehicle_num if populated and transport_id failed/empty @@ -223,7 +240,16 @@ class ConsolidadoImportacionMexService: # 3. Trailer (Placas Remolque) if logistics.trailer_num: - trl_obj = db.query(Trailer).filter(Trailer.trailer_number == logistics.trailer_num).first() + if logistics.trailer_int_id is not None: + trl_obj = ( + db.query(Trailer) + .filter(Trailer.trailer_id == logistics.trailer_int_id) + .first() + ) + else: + trl_obj = db.query(Trailer).filter( + Trailer.trailer_number == logistics.trailer_num + ).first() if trl_obj: placas_remolque_val = trl_obj.plate_number or "" diff --git a/backend/api/v1/modules/a76/reports/importacion/facturas/mex/service.py b/backend/api/v1/modules/a76/reports/importacion/facturas/mex/service.py index 185589d0..300f462e 100644 --- a/backend/api/v1/modules/a76/reports/importacion/facturas/mex/service.py +++ b/backend/api/v1/modules/a76/reports/importacion/facturas/mex/service.py @@ -317,7 +317,17 @@ class FacturaImportacionMexService: if logistics: # 1. Transporter (CAAT / SCAC) - if logistics.carrier_id: + if logistics.carrier_int_id is not None: + transporter_obj = ( + db.query(Transporter) + .filter(Transporter.transporter_id == logistics.carrier_int_id) + .first() + ) + if transporter_obj: + caat_val = transporter_obj.caat_code or "" + scac_val = transporter_obj.transport_code or "" + transportista_val = transporter_obj.name or logistics.carrier_id + elif logistics.carrier_id: transporter_obj = ( db.query(Transporter) .filter(Transporter.transporter_key == logistics.carrier_id) @@ -332,11 +342,18 @@ class FacturaImportacionMexService: # 2. Vehicle (Placas Tracto) - Try transport_id first if logistics.transport_id: - veh_obj = ( - db.query(Vehicle) - .filter(Vehicle.vehicle_key == logistics.transport_id) - .first() - ) + if logistics.transport_int_id is not None: + veh_obj = ( + db.query(Vehicle) + .filter(Vehicle.vehicle_id == logistics.transport_int_id) + .first() + ) + else: + veh_obj = ( + db.query(Vehicle) + .filter(Vehicle.vehicle_key == logistics.transport_id) + .first() + ) if veh_obj: placas_val = veh_obj.plate_number or placas_val elif ( @@ -352,11 +369,18 @@ class FacturaImportacionMexService: # 3. Trailer (Placas Remolque) if logistics.trailer_num: - trl_obj = ( - db.query(Trailer) - .filter(Trailer.trailer_number == logistics.trailer_num) - .first() - ) + if logistics.trailer_int_id is not None: + trl_obj = ( + db.query(Trailer) + .filter(Trailer.trailer_id == logistics.trailer_int_id) + .first() + ) + else: + trl_obj = ( + db.query(Trailer) + .filter(Trailer.trailer_number == logistics.trailer_num) + .first() + ) if trl_obj: placas_remolque_val = trl_obj.plate_number or "" diff --git a/backend/api/v1/modules/a76/reports/importacion/facturas/temporary/mex/service.py b/backend/api/v1/modules/a76/reports/importacion/facturas/temporary/mex/service.py index 192dab8a..3d0b86d5 100644 --- a/backend/api/v1/modules/a76/reports/importacion/facturas/temporary/mex/service.py +++ b/backend/api/v1/modules/a76/reports/importacion/facturas/temporary/mex/service.py @@ -166,8 +166,16 @@ class FacturaImportacionMexService: if logistics: # 1. Transporter (CAAT / SCAC) - if logistics.carrier_id: - transporter_obj = db.query(Transporter).filter(Transporter.transporter_key == logistics.carrier_id).first() + if logistics.carrier_int_id is not None: + transporter_obj = ( + db.query(Transporter) + .filter(Transporter.transporter_id == logistics.carrier_int_id) + .first() + ) + elif logistics.carrier_id: + transporter_obj = db.query(Transporter).filter( + Transporter.transporter_key == logistics.carrier_id + ).first() if transporter_obj: caat_val = transporter_obj.caat_code or "" scac_val = transporter_obj.transport_code or "" # Mapping transport_code to SCAC @@ -175,7 +183,16 @@ class FacturaImportacionMexService: # 2. Vehicle (Placas Tracto) - Try transport_id first if logistics.transport_id: - veh_obj = db.query(Vehicle).filter(Vehicle.vehicle_key == logistics.transport_id).first() + if logistics.transport_int_id is not None: + veh_obj = ( + db.query(Vehicle) + .filter(Vehicle.vehicle_id == logistics.transport_int_id) + .first() + ) + else: + veh_obj = db.query(Vehicle).filter( + Vehicle.vehicle_key == logistics.transport_id + ).first() if veh_obj: placas_val = veh_obj.plate_number or placas_val elif logistics.vehicle_num: # Fallback to vehicle_num if populated and transport_id failed/empty @@ -185,7 +202,16 @@ class FacturaImportacionMexService: # 3. Trailer (Placas Remolque) if logistics.trailer_num: - trl_obj = db.query(Trailer).filter(Trailer.trailer_number == logistics.trailer_num).first() + if logistics.trailer_int_id is not None: + trl_obj = ( + db.query(Trailer) + .filter(Trailer.trailer_id == logistics.trailer_int_id) + .first() + ) + else: + trl_obj = db.query(Trailer).filter( + Trailer.trailer_number == logistics.trailer_num + ).first() if trl_obj: placas_remolque_val = trl_obj.plate_number or "" diff --git a/backend/api/v1/modules/a76/reports/importacion/facturas/usa/service.py b/backend/api/v1/modules/a76/reports/importacion/facturas/usa/service.py index 9bd7c40d..8a4c1a91 100644 --- a/backend/api/v1/modules/a76/reports/importacion/facturas/usa/service.py +++ b/backend/api/v1/modules/a76/reports/importacion/facturas/usa/service.py @@ -317,7 +317,17 @@ class FacturaImportacionUsaService: if logistics: # 1. Transporter (CAAT / SCAC) - if logistics.carrier_id: + if logistics.carrier_int_id is not None: + transporter_obj = ( + db.query(Transporter) + .filter(Transporter.transporter_id == logistics.carrier_int_id) + .first() + ) + if transporter_obj: + caat_val = transporter_obj.caat_code or "" + scac_val = transporter_obj.transport_code or "" + transportista_val = transporter_obj.name or logistics.carrier_id + elif logistics.carrier_id: transporter_obj = ( db.query(Transporter) .filter(Transporter.transporter_key == logistics.carrier_id) @@ -325,18 +335,23 @@ class FacturaImportacionUsaService: ) if transporter_obj: caat_val = transporter_obj.caat_code or "" - scac_val = ( - transporter_obj.transport_code or "" - ) # Mapping transport_code to SCAC + scac_val = transporter_obj.transport_code or "" transportista_val = transporter_obj.name or logistics.carrier_id # 2. Vehicle (Plates) if logistics.transport_id: - veh_obj = ( - db.query(Vehicle) - .filter(Vehicle.vehicle_key == logistics.transport_id) - .first() - ) + if logistics.transport_int_id is not None: + veh_obj = ( + db.query(Vehicle) + .filter(Vehicle.vehicle_id == logistics.transport_int_id) + .first() + ) + else: + veh_obj = ( + db.query(Vehicle) + .filter(Vehicle.vehicle_key == logistics.transport_id) + .first() + ) if veh_obj: placas_val = veh_obj.plate_number or placas_val elif logistics.vehicle_num: @@ -350,11 +365,18 @@ class FacturaImportacionUsaService: # 3. Trailer if logistics.trailer_num: - trl_obj = ( - db.query(Trailer) - .filter(Trailer.trailer_number == logistics.trailer_num) - .first() - ) + if logistics.trailer_int_id is not None: + trl_obj = ( + db.query(Trailer) + .filter(Trailer.trailer_id == logistics.trailer_int_id) + .first() + ) + else: + trl_obj = ( + db.query(Trailer) + .filter(Trailer.trailer_number == logistics.trailer_num) + .first() + ) if trl_obj: placas_remolque_val = trl_obj.plate_number or "" diff --git a/backend/api/v1/modules/a76/reports/importacion/packing_list/service.py b/backend/api/v1/modules/a76/reports/importacion/packing_list/service.py index 8a74bbb3..822294ae 100644 --- a/backend/api/v1/modules/a76/reports/importacion/packing_list/service.py +++ b/backend/api/v1/modules/a76/reports/importacion/packing_list/service.py @@ -186,8 +186,18 @@ class PackingListService: if logistics: # 1. Transporter (CAAT / SCAC) - if logistics.carrier_id: - transporter_obj = db.query(Transporter).filter(Transporter.transporter_key == logistics.carrier_id).first() + if logistics.carrier_int_id is not None: + transporter_obj = ( + db.query(Transporter) + .filter(Transporter.transporter_id == logistics.carrier_int_id) + .first() + ) + elif logistics.carrier_id: + transporter_obj = ( + db.query(Transporter) + .filter(Transporter.transporter_key == logistics.carrier_id) + .first() + ) if transporter_obj: caat_val = transporter_obj.caat_code or "" scac_val = transporter_obj.transport_code or "" @@ -195,7 +205,18 @@ class PackingListService: # 2. Vehicle (Placas Tracto) if logistics.transport_id: - veh_obj = db.query(Vehicle).filter(Vehicle.vehicle_key == logistics.transport_id).first() + if logistics.transport_int_id is not None: + veh_obj = ( + db.query(Vehicle) + .filter(Vehicle.vehicle_id == logistics.transport_int_id) + .first() + ) + else: + veh_obj = ( + db.query(Vehicle) + .filter(Vehicle.vehicle_key == logistics.transport_id) + .first() + ) if veh_obj: placas_val = veh_obj.plate_number or placas_val elif logistics.vehicle_num: @@ -205,7 +226,18 @@ class PackingListService: # 3. Trailer (Placas Remolque) if logistics.trailer_num: - trl_obj = db.query(Trailer).filter(Trailer.trailer_number == logistics.trailer_num).first() + if logistics.trailer_int_id is not None: + trl_obj = ( + db.query(Trailer) + .filter(Trailer.trailer_id == logistics.trailer_int_id) + .first() + ) + else: + trl_obj = ( + db.query(Trailer) + .filter(Trailer.trailer_number == logistics.trailer_num) + .first() + ) if trl_obj: placas_remolque_val = trl_obj.plate_number or "" diff --git a/backend/api/v1/modules/a76/transportation/drivers/dto.py b/backend/api/v1/modules/a76/transportation/drivers/dto.py index 02420ea8..e67daee0 100644 --- a/backend/api/v1/modules/a76/transportation/drivers/dto.py +++ b/backend/api/v1/modules/a76/transportation/drivers/dto.py @@ -5,6 +5,7 @@ from pydantic import BaseModel class DriverBaseDTO(BaseModel): transporter_key: str + driver_id: Optional[int] = None line: int driver_name: Optional[str] = None license_number: Optional[str] = None diff --git a/backend/api/v1/modules/a76/transportation/drivers/models.py b/backend/api/v1/modules/a76/transportation/drivers/models.py index 7c70a9c3..dc6d7ee9 100644 --- a/backend/api/v1/modules/a76/transportation/drivers/models.py +++ b/backend/api/v1/modules/a76/transportation/drivers/models.py @@ -1,6 +1,6 @@ from api.v1.common.base_models import TenantScopedMixin, TimestampMixin from core.database import Base -from sqlalchemy import Column, ForeignKey, ForeignKeyConstraint, Integer, String +from sqlalchemy import BigInteger, Column, ForeignKey, ForeignKeyConstraint, Integer, String class Driver(Base, TenantScopedMixin, TimestampMixin): @@ -16,6 +16,8 @@ class Driver(Base, TenantScopedMixin, TimestampMixin): nullable=False, ) line = Column(Integer, primary_key=True, nullable=False) + # Internal integer surrogate ID. The frontend continues to use transporter_key/line. + driver_id = Column(BigInteger, nullable=False, unique=True, index=True) driver_name = Column(String(80), nullable=True) license_number = Column(String(29), nullable=True) express_line_id = Column(String(17), nullable=True) diff --git a/backend/api/v1/modules/a76/transportation/trailers/dto.py b/backend/api/v1/modules/a76/transportation/trailers/dto.py index 548f1d8f..662fb5b4 100644 --- a/backend/api/v1/modules/a76/transportation/trailers/dto.py +++ b/backend/api/v1/modules/a76/transportation/trailers/dto.py @@ -5,6 +5,7 @@ from pydantic import BaseModel, Field class TrailerBaseDTO(BaseModel): trailer_number: str = Field(..., description="Trailer number (primary identifier)") + trailer_id: Optional[int] = Field(None, description="Internal trailer integer ID") ace_trailer_number: Optional[str] = None trailer_type_key: Optional[str] = None seal: Optional[str] = None diff --git a/backend/api/v1/modules/a76/transportation/trailers/models.py b/backend/api/v1/modules/a76/transportation/trailers/models.py index d3b19e8b..19e7b1c9 100644 --- a/backend/api/v1/modules/a76/transportation/trailers/models.py +++ b/backend/api/v1/modules/a76/transportation/trailers/models.py @@ -1,6 +1,6 @@ from api.v1.common.base_models import TenantScopedMixin, TimestampMixin from core.database import Base -from sqlalchemy import Column, ForeignKey, ForeignKeyConstraint, String +from sqlalchemy import BigInteger, Column, ForeignKey, ForeignKeyConstraint, String class Trailer(Base, TenantScopedMixin, TimestampMixin): @@ -10,6 +10,8 @@ class Trailer(Base, TenantScopedMixin, TimestampMixin): ) trailer_number = Column(String(20), primary_key=True, nullable=False) + # Internal integer identifier (surrogate). The frontend continues to use trailer_number. + trailer_id = Column(BigInteger, nullable=False, unique=True, index=True) ace_trailer_number = Column(String(10), nullable=True) trailer_type_key = Column( String(2), ForeignKey("public.trailer_type.trailer_type_key"), nullable=True diff --git a/backend/api/v1/modules/a76/transportation/transporters/dto.py b/backend/api/v1/modules/a76/transportation/transporters/dto.py index 06391a6e..c4397d00 100644 --- a/backend/api/v1/modules/a76/transportation/transporters/dto.py +++ b/backend/api/v1/modules/a76/transportation/transporters/dto.py @@ -5,6 +5,7 @@ from pydantic import BaseModel, Field class TransporterBaseDTO(BaseModel): transporter_key: str = Field(..., description="Transporter key (primary identifier)") + transporter_id: Optional[int] = Field(None, description="Internal transporter integer ID") name: Optional[str] = None short_name: Optional[str] = None responsible: Optional[str] = None diff --git a/backend/api/v1/modules/a76/transportation/transporters/models.py b/backend/api/v1/modules/a76/transportation/transporters/models.py index 9f555c79..58d03367 100644 --- a/backend/api/v1/modules/a76/transportation/transporters/models.py +++ b/backend/api/v1/modules/a76/transportation/transporters/models.py @@ -1,6 +1,6 @@ from api.v1.common.base_models import TenantScopedMixin, TimestampMixin from core.database import Base -from sqlalchemy import Column, ForeignKeyConstraint, String +from sqlalchemy import BigInteger, Column, ForeignKeyConstraint, String class Transporter(Base, TenantScopedMixin, TimestampMixin): @@ -10,6 +10,8 @@ class Transporter(Base, TenantScopedMixin, TimestampMixin): ) transporter_key = Column(String(23), primary_key=True, nullable=False) + # Internal integer identifier (surrogate). The frontend continues to use transporter_key. + transporter_id = Column(BigInteger, nullable=False, unique=True, index=True) name = Column(String(256), nullable=True) short_name = Column(String(10), nullable=True) responsible = Column(String(100), nullable=True) diff --git a/backend/api/v1/modules/a76/transportation/vehicles/dto.py b/backend/api/v1/modules/a76/transportation/vehicles/dto.py index 7c7d28b9..c4cb6c8a 100644 --- a/backend/api/v1/modules/a76/transportation/vehicles/dto.py +++ b/backend/api/v1/modules/a76/transportation/vehicles/dto.py @@ -5,6 +5,7 @@ from pydantic import BaseModel, Field class VehicleBaseDTO(BaseModel): vehicle_key: str = Field(..., description="Vehicle key (primary identifier)") + vehicle_id: Optional[int] = Field(None, description="Internal vehicle integer ID") ace_vehicle_key: Optional[str] = None transporter_key: Optional[str] = None transport_identifier: Optional[str] = None diff --git a/backend/api/v1/modules/a76/transportation/vehicles/models.py b/backend/api/v1/modules/a76/transportation/vehicles/models.py index 9339daf8..0bbbb5d9 100644 --- a/backend/api/v1/modules/a76/transportation/vehicles/models.py +++ b/backend/api/v1/modules/a76/transportation/vehicles/models.py @@ -1,6 +1,6 @@ from api.v1.common.base_models import TenantScopedMixin, TimestampMixin from core.database import Base -from sqlalchemy import DECIMAL, Column, Integer, String +from sqlalchemy import DECIMAL, Column, Integer, BigInteger, String class Vehicle(Base, TenantScopedMixin, TimestampMixin): @@ -10,6 +10,8 @@ class Vehicle(Base, TenantScopedMixin, TimestampMixin): ) vehicle_key = Column(String(14), primary_key=True, nullable=False) + # Internal integer identifier (surrogate). The frontend continues to use vehicle_key. + vehicle_id = Column(BigInteger, nullable=False, unique=True, index=True) ace_vehicle_key = Column(String(10), nullable=True) transporter_key = Column(String(23), nullable=True) transport_identifier = Column(String(30), nullable=True) diff --git a/frontend/src/lib/api/dashboard/a76/invoices.ts b/frontend/src/lib/api/dashboard/a76/invoices.ts index d0849433..fb3c491c 100644 --- a/frontend/src/lib/api/dashboard/a76/invoices.ts +++ b/frontend/src/lib/api/dashboard/a76/invoices.ts @@ -124,7 +124,9 @@ export interface InvoiceLogistics { id?: number; invoice_id?: number; carrier_id?: string | null; + carrier_int_id?: number | null; transport_id?: string | null; + transport_int_id?: number | null; transport_us_id?: string | null; transport_type?: TransportType | null; transport_num?: string | null; @@ -136,6 +138,7 @@ export interface InvoiceLogistics { license_plate?: string | null; license_plate_complete?: string | null; trailer_num?: string | null; + trailer_int_id?: number | null; seal_number?: string | null; guide_number?: string | null; bill_number?: string | null; diff --git a/frontend/src/lib/api/dashboard/a76/trailers.ts b/frontend/src/lib/api/dashboard/a76/trailers.ts index 5d74aa2d..bf57509c 100644 --- a/frontend/src/lib/api/dashboard/a76/trailers.ts +++ b/frontend/src/lib/api/dashboard/a76/trailers.ts @@ -2,6 +2,7 @@ import { api, type ApiResponse } from '$lib/api'; export interface Trailer { trailer_number: string; + trailer_id?: number; ace_trailer_number?: string; trailer_type_key?: string; seal?: string; diff --git a/frontend/src/lib/api/dashboard/a76/transporters.ts b/frontend/src/lib/api/dashboard/a76/transporters.ts index fa585465..da855ae0 100644 --- a/frontend/src/lib/api/dashboard/a76/transporters.ts +++ b/frontend/src/lib/api/dashboard/a76/transporters.ts @@ -2,6 +2,7 @@ import { api, type ApiResponse } from '$lib/api'; export interface Transporter { transporter_key: string; + transporter_id?: number; name?: string; short_name?: string; responsible?: string; diff --git a/frontend/src/lib/api/dashboard/a76/vehicles.ts b/frontend/src/lib/api/dashboard/a76/vehicles.ts index 3fd7d67a..9c501dab 100644 --- a/frontend/src/lib/api/dashboard/a76/vehicles.ts +++ b/frontend/src/lib/api/dashboard/a76/vehicles.ts @@ -2,6 +2,7 @@ import { api, type ApiResponse } from '$lib/api'; export interface Vehicle { vehicle_key: string; + vehicle_id?: number; ace_vehicle_key?: string; transporter_key?: string; transport_identifier?: string;