diff --git a/backend/api/v1/modules/a24/fa/fa_item_lines/models.py b/backend/api/v1/modules/a24/fa/fa_item_lines/models.py index a0187a75..75496b34 100644 --- a/backend/api/v1/modules/a24/fa/fa_item_lines/models.py +++ b/backend/api/v1/modules/a24/fa/fa_item_lines/models.py @@ -75,9 +75,7 @@ class FaLineItem(Base, TenantScopedMixin, TimestampMixin): omit_annex31: Mapped[Optional[bool]] = mapped_column(Boolean) # OMITITENANEXO31 # --- RELACIÓN --- - master_info: Mapped["LineItem"] = relationship( - "api.v1.modules.a76.items.models.LineItem", back_populates="fa_data" - ) + master_info: Mapped["LineItem"] = relationship("LineItem", back_populates="fa_data") def __repr__(self) -> str: return f"" diff --git a/backend/api/v1/modules/a76/items/models.py b/backend/api/v1/modules/a76/items/models.py index 5eaa1180..7da837c3 100644 --- a/backend/api/v1/modules/a76/items/models.py +++ b/backend/api/v1/modules/a76/items/models.py @@ -214,7 +214,7 @@ class LineItem(Base, TenantScopedMixin, TimestampMixin): viewonly=True, ) fa_data: Mapped[Optional["FaLineItem"]] = relationship( - "api.v1.modules.a24.fa.fa_item_lines.models.FaLineItem", + "FaLineItem", back_populates="master_info", cascade="all, delete-orphan", uselist=False, diff --git a/backend/api/v1/modules/a76/reports/exportacion/aviso_consolidado/service.py b/backend/api/v1/modules/a76/reports/exportacion/aviso_consolidado/service.py index 4364dea8..3671361f 100644 --- a/backend/api/v1/modules/a76/reports/exportacion/aviso_consolidado/service.py +++ b/backend/api/v1/modules/a76/reports/exportacion/aviso_consolidado/service.py @@ -194,13 +194,12 @@ class AvisoConsolidadoExportacionService: # Calculate sum from items first if items: - for item in items: - for line in item: - if line.quantity and line.quantity.gross_weight: - try: - calculated_gross_weight += float(line.quantity.gross_weight) - except (ValueError, TypeError): - pass + for item in items: + if item.quantity and item.quantity.gross_weight: + try: + calculated_gross_weight += float(item.quantity.gross_weight) + except (ValueError, TypeError): + pass if financials and financials.gross_weight and float(financials.gross_weight) > 0: peso_bruto_val = f"{financials.gross_weight:,.2f}" 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 a4aeebc0..4a07a9a1 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 @@ -558,7 +558,7 @@ class ConsolidadoImportacionMexService: # --- Multi-Currency Normalization Logic --- # Determine Line Currency context # Use manual lookup instead of specific attribute - invoice_id = line.item.invoice_id if line.item else None + invoice_id = line.invoice_id line_invoice = invoice_map.get(invoice_id) if invoice_id else None line_currency_is_mxn = False 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 3170184a..17369185 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 @@ -370,7 +370,7 @@ class ConsolidadoImportacionMexService: # --- Multi-Currency Normalization Logic --- # Determine Line Currency context # Use manual lookup instead of specific attribute - invoice_id = line.item.invoice_id if line.item else None + invoice_id = line.invoice_id line_invoice = invoice_map.get(invoice_id) if invoice_id else None line_currency_is_mxn = False 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 24b46ea1..bdd5adca 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 @@ -35,6 +35,7 @@ from api.v1.modules.a76.general_catalogs.fractions.tariff_fractions.models impor # --- MODELO DE UNIDADES DE MEDIDA --- from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure +from api.v1.modules.a76.general_catalogs.packages.models import Package # --- SCHEMAS --- from .schemas import ( @@ -531,7 +532,7 @@ class FacturaImportacionMexService: if uom: unidad_desc = uom.description or uom.code else: - unidad_desc = "" + unidad_desc = "" partidas_list.append( PartidaSchema( @@ -550,7 +551,7 @@ class FacturaImportacionMexService: if qty and qty.package_quantity else 0 ), - clave_bultos=(qty.package_key or "") if qty else "", + clave_bultos=(qty.package_info.key if (qty and qty.package_info) else ""), peso_neto=self.formatear_numero(qty.net_weight if qty else 0), peso_bruto=self.formatear_numero( qty.gross_weight if qty else 0 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 d558f504..b61c2c87 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 @@ -315,7 +315,7 @@ class FacturaImportacionMexService: cantidad_importacion=self.formatear_numero(qty.quantity if qty else 0), unidad_medida=qty.weight_unit if qty else "PZA", cantidad_bultos=int(qty.package_quantity) if qty and qty.package_quantity else 0, - clave_bultos=(qty.package_key or "") if qty else "", + clave_bultos=(qty.package_info.key if (qty and qty.package_info) else ""), peso_neto=self.formatear_numero(qty.net_weight if qty else 0), peso_bruto=self.formatear_numero(qty.gross_weight if qty else 0), valor_costo_unitario=self.formatear_numero(v_unitario), 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 a4e6712e..fd45cd0f 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 @@ -10,17 +10,19 @@ from fastapi import HTTPException from sqlalchemy.orm import Session # --- MODELOS --- -from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceLogistics +from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceLogistics from api.v1.modules.a76.items.line_financials.models import LineFinancial from api.v1.modules.a76.items.line_quantities.models import LineQuantity from api.v1.modules.a76.clients_and_providers.models import ( - ClientProvider, ClientProviderAddress, ClientProviderPrograms + ClientProvider, + ClientProviderAddress, + ClientProviderPrograms, ) from api.v1.modules.a76.parts.models import Part from api.v1.modules.a76.pedmientos.models import Pedimentos from api.v1.modules.a76.general_catalogs.company.models import Company from api.v1.modules.a76.customs_brokers.models import CustomsBroker -from api.v1.modules.a76.items.models import LineItem +from api.v1.modules.a76.items.models import LineItem # --- TRANSPORTATION MODELS --- from api.v1.modules.a76.transportation.transporters.models import Transporter @@ -29,32 +31,38 @@ from api.v1.modules.a76.transportation.trailers.models import Trailer from api.v1.modules.a76.transportation.drivers.models import Driver # --- MODELO DE FRACCIONES --- -from api.v1.modules.a76.general_catalogs.fractions.tariff_fractions.models import TariffFraction +from api.v1.modules.a76.general_catalogs.fractions.tariff_fractions.models import ( + TariffFraction, +) # --- SCHEMAS --- # Reuse schemas from neighbor package as they fit the same data structure from ..mex.schemas import ( - ClienteSchema, PartidaSchema, TotalesSchema, - FacturaSchema, FacturaImportacionCompleta + ClienteSchema, + PartidaSchema, + TotalesSchema, + FacturaSchema, + FacturaImportacionCompleta, ) + class FacturaImportacionUsaService: def __init__(self): self.template_dir = Path(__file__).parent.parent / "templates" self.jinja_env = Environment( loader=FileSystemLoader(self.template_dir), - autoescape=select_autoescape(['html', 'xml']) + autoescape=select_autoescape(["html", "xml"]), ) - self.template = self.jinja_env.get_template('factura_usa_ver.html') + self.template = self.jinja_env.get_template("factura_usa_ver.html") def _get_document_title(self, invoice_type: str, is_american: bool = True) -> str: """ Determina el título del documento basado en el tipo de factura. - + Args: invoice_type: Tipo de factura (TEM, DEF, MEX, CR) is_american: Si es factura americana (True) o mexicana (False) - + Returns: Título formateado para la factura """ @@ -65,7 +73,7 @@ class FacturaImportacionUsaService: "TEM": "Importación Temporal", "CR": "Importación de Cambio de Régimen", } - + # Mapeo para facturas americanas american_titles = { "MEX": "Mexican Purchases Import Invoice", @@ -73,18 +81,18 @@ class FacturaImportacionUsaService: "TEM": "Temporary Importation", "CR": "Regime Change Importation", } - + # Seleccionar el mapa correcto titles = american_titles if is_american else mexican_titles - + # Obtener el título (normalizar a mayúsculas) invoice_type_upper = invoice_type.upper() if invoice_type else "" title = titles.get(invoice_type_upper, "") - + # Fallback a genéricos si no se encuentra if not title: return "Commercial Invoice" if is_american else "Factura de Importación" - + return title def _get_wkhtmltopdf_config(self): @@ -94,28 +102,49 @@ class FacturaImportacionUsaService: return pdfkit.configuration(wkhtmltopdf=path) def formatear_numero(self, valor, decimales: int = 2): - if valor is None: return 0.0 + if valor is None: + return 0.0 try: return round(float(valor), decimales) - except: return 0.0 + except: + return 0.0 def _format_fraccion_fallback(self, fraccion_raw: str) -> str: if not fraccion_raw or len(fraccion_raw) < 8: return fraccion_raw return f"{fraccion_raw[:4]}.{fraccion_raw[4:6]}.{fraccion_raw[6:]}" - def _obtener_datos_cliente(self, db: Session, client_id: int, rol: str) -> ClienteSchema: + def _obtener_datos_cliente( + self, db: Session, client_id: int, rol: str + ) -> ClienteSchema: main = db.query(ClientProvider).filter(ClientProvider.id == client_id).first() if not main: - return ClienteSchema(header=rol, nombre="Unknown", direccion="", tax_id="", codigo_postal="", ciudad="", estado="", pais="USA") - - addr = db.query(ClientProviderAddress).filter(ClientProviderAddress.client_id == client_id).first() - prog = db.query(ClientProviderPrograms).filter(ClientProviderPrograms.client_id == client_id).first() + return ClienteSchema( + header=rol, + nombre="Unknown", + direccion="", + tax_id="", + codigo_postal="", + ciudad="", + estado="", + pais="USA", + ) + + addr = ( + db.query(ClientProviderAddress) + .filter(ClientProviderAddress.client_id == client_id) + .first() + ) + prog = ( + db.query(ClientProviderPrograms) + .filter(ClientProviderPrograms.client_id == client_id) + .first() + ) return ClienteSchema( header=rol, nombre=(main.name or main.short_name) or "N/A", - direccion=(addr.streets or "") if addr else "", + direccion=(addr.streets or "") if addr else "", num_exterior=(addr.exterior_number or "") if addr else "", num_interior=(addr.interior_number or "") if addr else "", colonia=(addr.neighborhood or "") if addr else "", @@ -123,58 +152,128 @@ class FacturaImportacionUsaService: ciudad=(addr.city or "") if addr else "", estado=(addr.state or "") if addr else "", pais=(addr.country or "USA") if addr else "USA", - tax_id=prog.tax_id if (prog and prog.tax_id) else (getattr(main, 'rfc', "") or ""), - programa="IMMEX" if (prog and prog.program) else "", - autorizacion=prog.program_number if prog else "", - prosec=prog.prosec_authorization if (prog and prog.prosec and prog.prosec_authorization) else "", - reg_emp=prog.val_certified_company_registry if (prog and hasattr(prog, 'val_certified_company_registry')) else ( - prog.certified_company_registry if (prog and prog.certified_company_registry) else "" + tax_id=( + prog.tax_id + if (prog and prog.tax_id) + else (getattr(main, "rfc", "") or "") + ), + programa="IMMEX" if (prog and prog.program) else "", + autorizacion=prog.program_number if prog else "", + prosec=( + prog.prosec_authorization + if (prog and prog.prosec and prog.prosec_authorization) + else "" + ), + reg_emp=( + prog.val_certified_company_registry + if (prog and hasattr(prog, "val_certified_company_registry")) + else ( + prog.certified_company_registry + if (prog and prog.certified_company_registry) + else "" + ) + ), + cert=( + prog.is_certified_company + if (prog and prog.is_certified_company) + else "" ), - cert=prog.is_certified_company if (prog and prog.is_certified_company) else "" ) - def obtener_datos(self, db: Session, invoice_id: int, company_id: int, progress_callback: Optional[Callable] = None, currency_code: str = 'ORIGINAL') -> FacturaImportacionCompleta: + def obtener_datos( + self, + db: Session, + invoice_id: int, + company_id: int, + progress_callback: Optional[Callable] = None, + currency_code: str = "ORIGINAL", + ) -> FacturaImportacionCompleta: try: - if progress_callback: progress_callback(10, "Searching invoice...") - header = db.query(InvoiceHeader).filter(InvoiceHeader.id == invoice_id, InvoiceHeader.company_id == company_id).first() - if not header: raise HTTPException(status_code=404, detail="Invoice not found") + if progress_callback: + progress_callback(10, "Searching invoice...") + header = ( + db.query(InvoiceHeader) + .filter( + InvoiceHeader.id == invoice_id, + InvoiceHeader.company_id == company_id, + ) + .first() + ) + if not header: + raise HTTPException(status_code=404, detail="Invoice not found") - compliance = header.compliance_mx + compliance = header.compliance_mx logistics = header.logistics if header.logistics else None financials = header.financials if header.financials else None - if progress_callback: progress_callback(20, "Fetching entry data...") - pedimento_id = compliance.pedimento_id if (compliance and compliance.pedimento_id) else header.related_doc_id - pedimento = db.query(Pedimentos).filter(Pedimentos.id == pedimento_id).first() if pedimento_id else None - - if progress_callback: progress_callback(30, "Fetching client and supplier...") + if progress_callback: + progress_callback(20, "Fetching entry data...") + pedimento_id = ( + compliance.pedimento_id + if (compliance and compliance.pedimento_id) + else header.related_doc_id + ) + pedimento = ( + db.query(Pedimentos).filter(Pedimentos.id == pedimento_id).first() + if pedimento_id + else None + ) + + if progress_callback: + progress_callback(30, "Fetching client and supplier...") proveedor_id = compliance.provider_id if compliance else None - cliente_proveedor = self._obtener_datos_cliente(db, proveedor_id, "Supplier:") if proveedor_id else ClienteSchema(header="Supplier", nombre="Unassigned", direccion="", tax_id="", codigo_postal="", ciudad="", estado="", pais="") + cliente_proveedor = ( + self._obtener_datos_cliente(db, proveedor_id, "Supplier:") + if proveedor_id + else ClienteSchema( + header="Supplier", + nombre="Unassigned", + direccion="", + tax_id="", + codigo_postal="", + ciudad="", + estado="", + pais="", + ) + ) nombre_agente = "" if compliance and compliance.customs_broker_id: - broker = db.query(CustomsBroker).filter(CustomsBroker.id == compliance.customs_broker_id).first() - if broker: nombre_agente = broker.name + broker = ( + db.query(CustomsBroker) + .filter(CustomsBroker.id == compliance.customs_broker_id) + .first() + ) + if broker: + nombre_agente = broker.name company = db.query(Company).filter(Company.id == header.company_id).first() # Datos Default (Company/Importer) cliente_default = ClienteSchema( header="Importer / Consignee:", - nombre=getattr(company, 'name', "Local Company"), + nombre=getattr(company, "name", "Local Company"), direccion="FISCAL ADDRESS", - num_exterior="", colonia="", codigo_postal="", ciudad="", estado="", pais="MEX", - tax_id=getattr(company, 'rfc', ""), - programa=getattr(company, 'program', "IMMEX"), autorizacion=getattr(company, 'program_number', "") + num_exterior="", + colonia="", + codigo_postal="", + ciudad="", + estado="", + pais="MEX", + tax_id=getattr(company, "rfc", ""), + programa=getattr(company, "program", "IMMEX"), + autorizacion=getattr(company, "program_number", ""), ) # Left Side Logic (Sold To) cliente_vendido = cliente_default if compliance and compliance.sold_to_id: # Force English header for American Invoice - clean_header = "Sold To:" + clean_header = "Sold To:" # raw_header = compliance.sold_to_header or "SOLD_TO" # clean_header = raw_header.replace("_", " ").title() + ":" - cliente_vendido = self._obtener_datos_cliente(db, compliance.sold_to_id, clean_header) - + cliente_vendido = self._obtener_datos_cliente( + db, compliance.sold_to_id, clean_header + ) + # Right Side Logic (Shipped To) cliente_enviado = cliente_default if compliance and compliance.shipped_to_id: @@ -182,26 +281,37 @@ class FacturaImportacionUsaService: clean_header_shipped = "Shipped To:" # raw_header_shipped = compliance.shipped_to_header or "SHIPPED_TO" # clean_header_shipped = raw_header_shipped.replace("_", " ").title() + ":" - - # Fetch client data - cliente_enviado = self._obtener_datos_cliente(db, compliance.shipped_to_id, clean_header_shipped) - remesa_valor = str(compliance.remesa) if (compliance and compliance.remesa) else "" - acuse_valor = str(compliance.edocument) if (compliance and compliance.edocument) else "N/A" + # Fetch client data + cliente_enviado = self._obtener_datos_cliente( + db, compliance.shipped_to_id, clean_header_shipped + ) + + remesa_valor = ( + str(compliance.remesa) if (compliance and compliance.remesa) else "" + ) + acuse_valor = ( + str(compliance.edocument) + if (compliance and compliance.edocument) + else "N/A" + ) patente_val = "" if pedimento and pedimento.license: patente_val = pedimento.license - elif 'broker' in locals() and broker and broker.license: + elif "broker" in locals() and broker and broker.license: patente_val = broker.license - # --- Transport Data Fetching --- - transporte_txt = str(logistics.transport_type) if (logistics and logistics.transport_type) else "" + transporte_txt = ( + str(logistics.transport_type) + if (logistics and logistics.transport_type) + else "" + ) num_transporte_val = (logistics.trailer_num or "") if logistics else "" - + # Init values - placas_val = (logistics.license_plate or "") if logistics else "" # Plates + placas_val = (logistics.license_plate or "") if logistics else "" # Plates placas_remolque_val = "" transportista_val = (logistics.carrier_id or "") if logistics else "" caat_val = "" @@ -211,53 +321,89 @@ class FacturaImportacionUsaService: if logistics: # 1. Transporter (CAAT / SCAC) if logistics.carrier_id: - transporter_obj = db.query(Transporter).filter(Transporter.transporter_key == logistics.carrier_id).first() + 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 + scac_val = ( + transporter_obj.transport_code or "" + ) # Mapping transport_code to SCAC 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() + 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: - veh_obj = db.query(Vehicle).filter(Vehicle.vehicle_key == logistics.vehicle_num).first() - if veh_obj: - placas_val = veh_obj.plate_number or placas_val + placas_val = veh_obj.plate_number or placas_val + elif logistics.vehicle_num: + veh_obj = ( + db.query(Vehicle) + .filter(Vehicle.vehicle_key == logistics.vehicle_num) + .first() + ) + if veh_obj: + placas_val = veh_obj.plate_number or placas_val # 3. Trailer if logistics.trailer_num: - trl_obj = db.query(Trailer).filter(Trailer.trailer_number == logistics.trailer_num).first() + trl_obj = ( + db.query(Trailer) + .filter(Trailer.trailer_number == logistics.trailer_num) + .first() + ) if trl_obj: placas_remolque_val = trl_obj.plate_number or "" # 4. Driver (License) if logistics.carrier_id and logistics.driver_name: - drv_obj = db.query(Driver).filter( - Driver.transporter_key == logistics.carrier_id, - Driver.driver_name == logistics.driver_name - ).first() + drv_obj = ( + db.query(Driver) + .filter( + Driver.transporter_key == logistics.carrier_id, + Driver.driver_name == logistics.driver_name, + ) + .first() + ) if drv_obj: - licencia_cond_val = drv_obj.license_number or "" + licencia_cond_val = drv_obj.license_number or "" # Determine Currency - moneda_final = getattr(header, 'currency', "USD") or "USD" - if currency_code == 'MXN': - moneda_final = 'MXN' - elif currency_code == 'USD': - moneda_final = 'USD' + moneda_final = getattr(header, "currency", "USD") or "USD" + if currency_code == "MXN": + moneda_final = "MXN" + elif currency_code == "USD": + moneda_final = "USD" factura_schema = FacturaSchema( numero=header.invoice_number or "N/A", - titulo_documento=self._get_document_title(header.invoice_type or "", is_american=True), + titulo_documento=self._get_document_title( + header.invoice_type or "", is_american=True + ), fecha=str(header.invoice_date) if header.invoice_date else "", - tipo_cambio=float(financials.exchange_rate) if (financials and financials.exchange_rate) else (float(pedimento.exchange_rate) if pedimento and pedimento.exchange_rate else 1.0), + tipo_cambio=( + float(financials.exchange_rate) + if (financials and financials.exchange_rate) + else ( + float(pedimento.exchange_rate) + if pedimento and pedimento.exchange_rate + else 1.0 + ) + ), moneda=moneda_final, incoterm=(logistics.incoterm or "") if logistics else "", observaciones=header.observation_es or header.observation_en or "", - pedimento=f"{pedimento.year} {pedimento.customs_office[:2] if pedimento.customs_office else ''} {pedimento.license} {pedimento.pedimento_number}" if pedimento else "", + pedimento=( + f"{pedimento.year} {pedimento.customs_office[:2] if pedimento.customs_office else ''} {pedimento.license} {pedimento.pedimento_number}" + if pedimento + else "" + ), clave_pedimento=pedimento.pedimento_code if pedimento else "", regimen=header.document_type or "", patente=patente_val, @@ -270,62 +416,87 @@ class FacturaImportacionUsaService: caat=caat_val, scac=scac_val, licencia_conductor=licencia_cond_val, - aduana=compliance.aduana if (compliance and compliance.aduana) else (pedimento.customs_office[:2] if (pedimento and pedimento.customs_office) else ""), + aduana=( + compliance.aduana + if (compliance and compliance.aduana) + else ( + pedimento.customs_office[:2] + if (pedimento and pedimento.customs_office) + else "" + ) + ), precinto=(logistics.seal_number or "") if logistics else "", destino=(logistics.destination_goods or "") if logistics else "", - remesa=remesa_valor, acuse_electronico=acuse_valor + remesa=remesa_valor, + acuse_electronico=acuse_valor, ) - - if progress_callback: progress_callback(50, "Processing items...") + + if progress_callback: + progress_callback(50, "Processing items...") lines = db.query(LineItem).filter(LineItem.invoice_id == header.id).all() partidas_list = [] - + for line in lines: - qty = db.query(LineQuantity).filter(LineQuantity.item_line_id == line.id).first() - fin = db.query(LineFinancial).filter(LineFinancial.item_line_id == line.id).first() - part_master = db.query(Part).filter(Part.id == line.part_number_id).first() + qty = ( + db.query(LineQuantity) + .filter(LineQuantity.item_line_id == line.id) + .first() + ) + fin = ( + db.query(LineFinancial) + .filter(LineFinancial.item_line_id == line.id) + .first() + ) + part_master = ( + db.query(Part).filter(Part.id == line.part_number_id).first() + ) desc_final = "N/D" - num_parte_final = str(line.part_nupart_number_idmber or "N/A") - fraccion_raw = "" + num_parte_final = str(line.part_number_id or "N/A") + fraccion_raw = "" origen_final = "MEX" if part_master: # Prefer English description if available, else Spanish - desc_final = part_master.description_english or part_master.description_spanish or "No Desc." + desc_final = ( + part_master.description_english + or part_master.description_spanish + or "No Desc." + ) num_parte_final = part_master.part_number # Prefer US Fraction (HTS) if available - fraccion_raw = part_master.us_fraction if part_master.us_fraction else "" - + fraccion_raw = ( + part_master.us_fraction if part_master.us_fraction else "" + ) + if part_master.fa_data and part_master.fa_data.origin_country: origen_final = part_master.fa_data.origin_country - # FRACTION LOGIC: Use US Fraction (us_fraction) if available, otherwise blank fraccion_imprimir = "" - + # Check part master US fraction if part_master and part_master.us_fraction: fraccion_imprimir = part_master.us_fraction.strip() - + # Optional: Format if needed, but raw is usually fine for US HTS # If valid US fraction logic requires looking up in DB, we could add that here. # For now, per requirement: "Si no tiene, pues de queda en blanco" - + # Default "General" and "0%" if no specific logic for US duties yet - preferencia_txt = "General" + preferencia_txt = "General" advalorem_txt = "0%" # Prioritize USD for American Invoice logic if available? # Sticking to same logic as Mex for now but could prioritize USD columns. # Actually, duplicate logic from mex service for now to ensure consistency. - + v_unitario = 0.0 v_total = 0.0 - + if fin: - is_mxn = (factura_schema.moneda == 'MXN') - + is_mxn = factura_schema.moneda == "MXN" + if is_mxn: v_unitario = float(fin.unit_cost_commercial_mxn or 0.0) v_total = float(fin.value_commercial_mxn or 0.0) @@ -334,13 +505,13 @@ class FacturaImportacionUsaService: v_total = float(fin.value_commercial_usd or 0.0) if not v_unitario: - v_unitario = float(fin.commercial_unit_cost or 0.0) - + v_unitario = float(fin.commercial_unit_cost or 0.0) + if not v_total: - v_total = float(fin.total_commercial_value or 0.0) + v_total = float(fin.total_commercial_value or 0.0) cantidad = float(qty.quantity) if (qty and qty.quantity) else 0.0 - + if cantidad > 0: if v_unitario > 0 and v_total == 0: v_total = v_unitario * cantidad @@ -348,39 +519,59 @@ class FacturaImportacionUsaService: v_unitario = v_total / cantidad # UOM Mapping for English context - uom_raw = qty.weight_unit if qty else "PCS" - if uom_raw == "PZA": uom_raw = "PCS" + uom_raw = line.unit_of_measure_info.code if line.unit_of_measure_info else "PCS" + if uom_raw == "PZA": + uom_raw = "PCS" - partidas_list.append(PartidaSchema( - numero_parte=num_parte_final, - descripcion=desc_final, - fraccion=fraccion_imprimir, - origen=origen_final, - advalorem=advalorem_txt, - preferencia=preferencia_txt, - cantidad_importacion=self.formatear_numero(qty.quantity if qty else 0), - unidad_medida=uom_raw, - cantidad_bultos=int(qty.package_quantity) if qty and qty.package_quantity else 0, - clave_bultos=(qty.package_key or "") if qty else "", - peso_neto=self.formatear_numero(qty.net_weight if qty else 0), - peso_bruto=self.formatear_numero(qty.gross_weight if qty else 0), - valor_costo_unitario=self.formatear_numero(v_unitario), - valor_total=self.formatear_numero(v_total) - )) + partidas_list.append( + PartidaSchema( + numero_parte=num_parte_final, + descripcion=desc_final, + fraccion=fraccion_imprimir, + origen=origen_final, + advalorem=advalorem_txt, + preferencia=preferencia_txt, + cantidad_importacion=self.formatear_numero( + qty.quantity if qty else 0 + ), + unidad_medida=uom_raw, + cantidad_bultos=( + int(qty.package_quantity) + if qty and qty.package_quantity + else 0 + ), + clave_bultos=( + qty.package_info.key if (qty and qty.package_info) else "" + ), + peso_neto=self.formatear_numero(qty.net_weight if qty else 0), + peso_bruto=self.formatear_numero( + qty.gross_weight if qty else 0 + ), + valor_costo_unitario=self.formatear_numero(v_unitario), + valor_total=self.formatear_numero(v_total), + ) + ) - totales = self.calcular_totales(partidas_list, Decimal(factura_schema.tipo_cambio)) + totales = self.calcular_totales( + partidas_list, Decimal(factura_schema.tipo_cambio) + ) return FacturaImportacionCompleta( - cliente_proveedor=cliente_proveedor, cliente_vendido=cliente_vendido, - cliente_enviado=cliente_enviado, factura=factura_schema, - partidas=partidas_list, totales=totales + cliente_proveedor=cliente_proveedor, + cliente_vendido=cliente_vendido, + cliente_enviado=cliente_enviado, + factura=factura_schema, + partidas=partidas_list, + totales=totales, ) except Exception as e: print(f"Error Service A76 USA: {e}") raise HTTPException(status_code=500, detail=f"Error: {str(e)}") - def calcular_totales(self, partidas: List[PartidaSchema], tipo_cambio: Decimal) -> TotalesSchema: + def calcular_totales( + self, partidas: List[PartidaSchema], tipo_cambio: Decimal + ) -> TotalesSchema: cant = sum(p.cantidad_importacion for p in partidas) valor = sum(p.valor_total for p in partidas) peso_n = sum(p.peso_neto for p in partidas) @@ -388,22 +579,38 @@ class FacturaImportacionUsaService: bultos = sum(p.cantidad_bultos for p in partidas) claves = [p.clave_bultos for p in partidas if p.clave_bultos] clave_comun = max(set(claves), key=claves.count) if claves else "" - # if bultos > 1 and clave_comun and not clave_comun.endswith("S"): clave_comun += "S" + # if bultos > 1 and clave_comun and not clave_comun.endswith("S"): clave_comun += "S" # Don't pluralize strictly in English without logic, kept simple. - + tc = float(tipo_cambio) if tipo_cambio else 1.0 - return TotalesSchema( - cantidad_total=self.formatear_numero(cant), bultos_total=bultos, clave_bultos=clave_comun, - peso_neto_total=self.formatear_numero(peso_n), peso_bruto_total=self.formatear_numero(peso_b), - valor_total_total=self.formatear_numero(valor), valor_total_dolares=self.formatear_numero(valor / tc if tc > 0 else 0) + return TotalesSchema( + cantidad_total=self.formatear_numero(cant), + bultos_total=bultos, + clave_bultos=clave_comun, + peso_neto_total=self.formatear_numero(peso_n), + peso_bruto_total=self.formatear_numero(peso_b), + valor_total_total=self.formatear_numero(valor), + valor_total_dolares=self.formatear_numero(valor / tc if tc > 0 else 0), ) - def generar_factura_completa(self, db: Session, invoice_id: int, company_id: int, formato: str = "pdf", progress_callback: Optional[Callable] = None, currency_code: str = 'ORIGINAL') -> Tuple[bytes, str, str]: - if progress_callback: progress_callback(5, "Starting report service...") - datos = self.obtener_datos(db, invoice_id, company_id, progress_callback, currency_code) - - if progress_callback: progress_callback(80, "Rendering template...") - + def generar_factura_completa( + self, + db: Session, + invoice_id: int, + company_id: int, + formato: str = "pdf", + progress_callback: Optional[Callable] = None, + currency_code: str = "ORIGINAL", + ) -> Tuple[bytes, str, str]: + if progress_callback: + progress_callback(5, "Starting report service...") + datos = self.obtener_datos( + db, invoice_id, company_id, progress_callback, currency_code + ) + + if progress_callback: + progress_callback(80, "Rendering template...") + # LOGO LOGIC logo_b64 = None try: @@ -418,26 +625,48 @@ class FacturaImportacionUsaService: if target_path.exists(): with open(target_path, "rb") as image_file: - encoded_string = base64.b64encode(image_file.read()).decode('utf-8') + encoded_string = base64.b64encode(image_file.read()).decode( + "utf-8" + ) mime = "image/png" - if target_path.suffix.lower() in ['.jpg', '.jpeg']: mime = "image/jpeg" + if target_path.suffix.lower() in [".jpg", ".jpeg"]: + mime = "image/jpeg" logo_b64 = f"data:{mime};base64,{encoded_string}" except Exception as e: print(f"Error loading logo: {e}") context = { - 'cliente_proveedor': datos.cliente_proveedor.model_dump(), 'cliente_vendido': datos.cliente_vendido.model_dump(), - 'cliente_enviado': datos.cliente_enviado.model_dump(), 'factura': datos.factura.model_dump(), - 'partidas': [p.model_dump() for p in datos.partidas], 'totales': datos.totales.model_dump(), - 'logo_b64': logo_b64 + "cliente_proveedor": datos.cliente_proveedor.model_dump(), + "cliente_vendido": datos.cliente_vendido.model_dump(), + "cliente_enviado": datos.cliente_enviado.model_dump(), + "factura": datos.factura.model_dump(), + "partidas": [p.model_dump() for p in datos.partidas], + "totales": datos.totales.model_dump(), + "logo_b64": logo_b64, } html_content = self.template.render(**context) nombre = f"Commercial_Invoice_{datos.factura.numero}.{formato}" - if formato == "html": return html_content.encode('utf-8'), nombre, "text/html" - - if progress_callback: progress_callback(90, "Generating PDF...") - options = {'page-size': 'Letter', 'margin-top': '0.5in', 'margin-right': '0.5in', 'margin-bottom': '0.5in', 'margin-left': '0.5in', 'encoding': "UTF-8", 'enable-local-file-access': None} - pdf = pdfkit.from_string(html_content, False, options=options, configuration=self._get_wkhtmltopdf_config()) - - if progress_callback: progress_callback(100, "Completed") + if formato == "html": + return html_content.encode("utf-8"), nombre, "text/html" + + if progress_callback: + progress_callback(90, "Generating PDF...") + options = { + "page-size": "Letter", + "margin-top": "0.5in", + "margin-right": "0.5in", + "margin-bottom": "0.5in", + "margin-left": "0.5in", + "encoding": "UTF-8", + "enable-local-file-access": None, + } + pdf = pdfkit.from_string( + html_content, + False, + options=options, + configuration=self._get_wkhtmltopdf_config(), + ) + + if progress_callback: + progress_callback(100, "Completed") return pdf, nombre, "application/pdf" 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 b88cb83c..8500c2d4 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 @@ -241,29 +241,29 @@ class PackingListService: partidas_list = [] for line in lines: - qty = db.query(LineQuantity).filter(LineQuantity.item_line_id == line.id).first() + weight_type = db.query(InvoiceLogistics.weight_type).filter(InvoiceLogistics.invoice_id == line.invoice_id).scalar() + qty = db.query(LineQuantity).filter(LineQuantity.item_line_id == line.id).first() # --- WEIGHT CALCULATION LOGIC --- peso_neto_kg = 0.0 peso_bruto_kg = 0.0 peso_neto_lb = 0.0 - peso_bruto_lb = 0.0 + peso_bruto_lb = 0.0 - if qty: - raw_net = float(qty.net_weight or 0) - raw_gross = float(qty.gross_weight or 0) - unit = (qty.weight_unit or "KG").upper() - - if unit == "LB" or unit == "LBS": - peso_neto_lb = raw_net - peso_bruto_lb = raw_gross - peso_neto_kg = raw_net / 2.20462 - peso_bruto_kg = raw_gross / 2.20462 - else: # Default KG - peso_neto_kg = raw_net - peso_bruto_kg = raw_gross - peso_neto_lb = raw_net * 2.20462 - peso_bruto_lb = raw_gross * 2.20462 + raw_net = float(qty.net_weight or 0) + raw_gross = float(qty.gross_weight or 0) + unit = (weight_type or "KGS").upper() + + if unit == "LBS": + peso_neto_lb = raw_net + peso_bruto_lb = raw_gross + peso_neto_kg = raw_net / 2.20462 + peso_bruto_kg = raw_gross / 2.20462 + else: # Default KG + peso_neto_kg = raw_net + peso_bruto_kg = raw_gross + peso_neto_lb = raw_net * 2.20462 + peso_bruto_lb = raw_gross * 2.20462 # -------------------------------- custom_obj = db.query(LineCustom).filter(LineCustom.item_line_id == line.id).first() diff --git a/backend/core/celery_app.py b/backend/core/celery_app.py index e0d19372..47ad480d 100644 --- a/backend/core/celery_app.py +++ b/backend/core/celery_app.py @@ -1,6 +1,10 @@ import os from celery import Celery +# Import models in correct order for SQLAlchemy relationship resolution +# CRITICAL: FaLineItem must be imported BEFORE LineItem +from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem # noqa: F401 +from api.v1.modules.a76.items.models import LineItem # noqa: F401 valkey_url = os.getenv("VALKEY_URL", "redis://valkey:6379/0") @@ -13,8 +17,8 @@ celery_app = Celery( "api.v1.modules.a76.reports.importacion.consolidados.task", "api.v1.modules.a76.reports.importacion.packing_list.task", "api.v1.modules.a76.reports.exportacion.aviso_consolidado.task", - "api.v1.modules.a76.reports.exportacion.descargo.task" - ] # Ruta al módulo donde están las tareas + "api.v1.modules.a76.reports.exportacion.descargo.task", + ], # Ruta al módulo donde están las tareas ) # Configuraciones adicionales @@ -28,4 +32,4 @@ celery_app.conf.update( ) if __name__ == "__main__": - celery_app.start() \ No newline at end of file + celery_app.start() diff --git a/backend/main.py b/backend/main.py index af6c9199..a77f6cb6 100644 --- a/backend/main.py +++ b/backend/main.py @@ -23,6 +23,8 @@ from fastapi.staticfiles import StaticFiles from pathlib import Path # Importar modelos para registrar con SQLAlchemy +# IMPORTANT: Import FaLineItem BEFORE LineItem for relationship resolution +from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem from api.v1.modules.a76.items.models import LineItem from api.v1.modules.a76.items.series.models import Serie from api.v1.modules.a76.parts.models import Part @@ -83,6 +85,7 @@ async def on_startup(): run_migrations() logger.info("Base de datos inicializada correctamente.") + # Configurar CORS app.add_middleware( CORSMiddleware,