diff --git a/backend/api/v1/modules/a76/reports/importacion/packing_list/schemas.py b/backend/api/v1/modules/a76/reports/importacion/packing_list/schemas.py index cbbd0958..fcaaae40 100644 --- a/backend/api/v1/modules/a76/reports/importacion/packing_list/schemas.py +++ b/backend/api/v1/modules/a76/reports/importacion/packing_list/schemas.py @@ -54,6 +54,7 @@ class PartidaSchema(BaseModel): numero_parte: str descripcion: str fraccion: str + fraccion_americana: Optional[str] = "" origen: str advalorem:Optional[str] = "" @@ -65,6 +66,8 @@ class PartidaSchema(BaseModel): clave_bultos: str peso_neto: Union[float, str] peso_bruto: Union[float, str] + peso_neto_lbs: Union[float, str] = 0.0 + peso_bruto_lbs: Union[float, str] = 0.0 valor_costo_unitario: Union[float, str] = "" valor_total: Union[float, str] = "" @@ -74,6 +77,8 @@ class TotalesSchema(BaseModel): clave_bultos: str = "" peso_neto_total: Union[float, str] peso_bruto_total: Union[float, str] + peso_neto_total_lbs: Union[float, str] = 0.0 + peso_bruto_total_lbs: Union[float, str] = 0.0 valor_total_total: Union[float, str] = "" valor_total_dolares: Union[float, str] = "" 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 a31a1eb2..43f0036d 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 @@ -3,7 +3,7 @@ import base64 import pdfkit from pathlib import Path from decimal import Decimal -from typing import Tuple, List, Callable, Optional, Dict +from typing import Tuple, List, Callable, Optional from jinja2 import Environment, FileSystemLoader, select_autoescape from fastapi import HTTPException @@ -12,9 +12,9 @@ from sqlalchemy.orm import Session # --- MODELOS --- 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.items.line_items.models import LineItem +from api.v1.modules.a76.items.line_customs.models import LineCustom from api.v1.modules.a76.clients_and_providers.models import ( ClientProvider, ClientProviderAddress, ClientProviderPrograms ) @@ -60,7 +60,6 @@ class PackingListService: path = next((p for p in paths if p and Path(p).exists()), None) if not path: - # If we are in dev and cannot find it, try to mock it or raise clearer error if shutil.which("echo"): print("WARNING: wkhtmltopdf not found, PDF generation will fail.") raise RuntimeError(f"wkhtmltopdf binary not found. Searched in: {paths}") @@ -133,7 +132,7 @@ class PackingListService: company = db.query(Company).filter(Company.id == header.company_id).first() # Datos Default (Company/Importer) cliente_default = ClienteSchema( - header="Importador / consignatario:", + header="Importer / Consignee:", nombre=getattr(company, 'name', "Empresa Local"), direccion="DOMICILIO FISCAL", num_exterior="", colonia="", codigo_postal="", ciudad="", estado="", pais="MEX", @@ -144,15 +143,18 @@ class PackingListService: # Left Side Logic (Consignatario / Sold To) cliente_vendido = cliente_default if compliance and compliance.sold_to_id: - raw_header = compliance.sold_to_header or "CONSIGNATARIO" - clean_header = raw_header.replace("_", " ").capitalize() + ":" + raw = (compliance.sold_to_header or "").upper() + if "CONSIGN" in raw: + clean_header = "Consignee / Consignatario:" + else: + clean_header = "Sold To / Vendido a:" + cliente_vendido = self._obtener_datos_cliente(db, compliance.sold_to_id, clean_header) # Right Side Logic (Enviado A / Shipped To) cliente_enviado = cliente_default if compliance and compliance.shipped_to_id: - raw_header_shipped = compliance.shipped_to_header or "DESTINATARIO" - clean_header_shipped = raw_header_shipped.replace("_", " ").capitalize() + ":" + clean_header_shipped = "Shipped To / Enviado a:" 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 "" @@ -241,17 +243,45 @@ class PackingListService: for line in lines: 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 + + 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 + # -------------------------------- + + custom_obj = db.query(LineCustom).filter(LineCustom.item_line_id == line.id).first() part_master = db.query(Part).filter(Part.id == line.part_number).first() desc_final = "S/D" num_parte_final = str(line.part_number or "S/N") fraccion_raw = "" origen_final = "MEX" + uom_comercial = "PZA" # Default UOM if part_master: desc_final = part_master.description_spanish or part_master.description_english or "Sin Desc." num_parte_final = part_master.part_number fraccion_raw = part_master.fraction if part_master.fraction else "" + # Commercial UOM from Part Master + uom_comercial = part_master.unit_of_measure or "PZA" if part_master.fa_data and part_master.fa_data.origin_country: origen_final = part_master.fa_data.origin_country @@ -276,15 +306,18 @@ class PackingListService: numero_parte=num_parte_final, descripcion=desc_final, fraccion=fraccion_imprimir, + fraccion_americana=custom_obj.american_fraction if custom_obj and custom_obj.american_fraction else "", origen=origen_final, advalorem="", # Hidden preferencia="", # Hidden cantidad_importacion=qty.quantity if qty else 0, - unidad_medida=qty.weight_unit if qty else "PZA", + unidad_medida=uom_comercial, # Commercial UOM (PCS, EA) 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=qty.net_weight if qty else 0, - peso_bruto=qty.gross_weight if qty else 0, + peso_neto=self.formatear_numero(peso_neto_kg), + peso_bruto=self.formatear_numero(peso_bruto_kg), + peso_neto_lbs=self.formatear_numero(peso_neto_lb), + peso_bruto_lbs=self.formatear_numero(peso_bruto_lb), valor_costo_unitario=v_unitario, # Hidden valor_total=v_total # Hidden )) @@ -309,6 +342,9 @@ class PackingListService: # Financial totals hidden peso_n = sum(float(p.peso_neto) for p in partidas) peso_b = sum(float(p.peso_bruto) for p in partidas) + peso_n_lbs = sum(float(p.peso_neto_lbs) for p in partidas) + peso_b_lbs = sum(float(p.peso_bruto_lbs) for p in partidas) + bultos = sum(p.cantidad_bultos for p in partidas) claves = [p.clave_bultos for p in partidas if p.clave_bultos] @@ -318,6 +354,7 @@ class PackingListService: 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), + peso_neto_total_lbs=self.formatear_numero(peso_n_lbs), peso_bruto_total_lbs=self.formatear_numero(peso_b_lbs), valor_total_total="", valor_total_dolares="" ) diff --git a/backend/api/v1/modules/a76/reports/importacion/packing_list/templates/packing_list.html b/backend/api/v1/modules/a76/reports/importacion/packing_list/templates/packing_list.html index 4ed057d9..a82f2aaa 100644 --- a/backend/api/v1/modules/a76/reports/importacion/packing_list/templates/packing_list.html +++ b/backend/api/v1/modules/a76/reports/importacion/packing_list/templates/packing_list.html @@ -278,8 +278,26 @@
-
|
+ PACKING LIST / LISTA DE EMPAQUE: + |
+
+ {{ factura.numero }} + |
+ ||
|
+ MX CUSTOM BROKER / AGENTE ADUANAL MEXICANO: + |
+
+ {{ factura.agente_aduanal or '' }} + |
+ ||
{{ cliente_proveedor.nombre }}
{{ cliente_proveedor.direccion }} @@ -308,97 +326,7 @@
|
- PACKING LIST / LISTA DE EMPAQUE: - |
-
- {{ factura.numero }} - |
- ||
|
- Fecha: - |
-
- {{ factura.fecha }} - |
-
- T. Cambio: - |
-
- {{ factura.tipo_cambio }} - |
-
|
- Pedimento: - |
-
- {{ factura.pedimento or '' }} - |
-
- Clave: - |
-
- {{ factura.clave_pedimento or '' }} - |
-
|
- Remesa: - |
-
- {{ factura.remesa or '' }} - |
-
- Acuse: - |
-
- {{ factura.acuse_electronico or 'N/A' }} - |
-
|
- Mx custom broker / agente aduanal mexicano: -{{ factura.agente_aduanal or '' }} - |
- |||
|
- Patente: {{ factura.patente or '' }} - |
- - Regimen:{{ - factura.regimen or '' }} - | -
- INCOTERM: -{{ factura.incoterm or '' }} - |
- |
|
- {% if factura.precinto %}
- Precinto: {{ factura.precinto }} - {% endif %} - |
-
- Aduana: {{ factura.aduana or '' }} - |
-
- {% if factura.destino %}
- Destino: {{ factura.destino }} - {% endif %} - |
- |
|
+ Line / Línea + |
+
+ Part Number / Número de Parte +Description / Descripción + |
+
+ Quantity / Cantidad + |
+
+ Packing / Empaque + |
+
+ Weight / Peso (KGS) + |
+
+ |||||
|
+ Qty / Cant. + |
+
+ U.M. + |
+
+ Qty / Cant. + |
+
+ Type / Tipo + |
+
+ Net / Neto +(LBS / KGS) + |
+
+ Gross / Bruto +(LBS / KGS) + |
+ ||||
|
- Línea - |
-
- Número de Parte -Descripción - |
-
- Comercial - |
-
- Empaque - |
-
- Peso (KGS) - |
-
- |||||
|
- Cantidad - |
-
- U.M. - |
-
- Cant. - |
-
- Tipo - |
-
- Neto - |
-
- Bruto - |
- ||||
|
+ {{ loop.index }} + |
+
+ {{ partida.numero_parte }} +{{ partida.descripcion }} / {{ partida.fraccion_americana }} / {{ partida.origen }} + + |
+
+ {{ partida.cantidad_importacion }} + |
+
+ {{ partida.unidad_medida }} + |
+
+ + {% if partida.cantidad_bultos != 0 %}{{ partida.cantidad_bultos }}{% endif %} + + |
+
+ + {{ partida.clave_bultos }} + + |
+
+ {{ partida.peso_neto_lbs }} +{{ partida.peso_neto }} + |
+
+ {{ partida.peso_bruto_lbs }} +{{ partida.peso_bruto }} + |
+ ||
|
- {{ loop.index }} - |
-
- {{ partida.numero_parte }} -{{ partida.descripcion }} - |
-
- {{ partida.cantidad_importacion }} - |
-
- {{ partida.unidad_medida }} - |
-
- - {% if partida.cantidad_bultos != 0 %}{{ partida.cantidad_bultos }}{% endif %} - - |
-
- - {{ partida.clave_bultos }} - - |
-
- {{ partida.peso_neto }} - |
-
- {{ partida.peso_bruto }} - |
- ||
|
- - Observaciones: - TOTALES - - |
-
- {{ totales.cantidad_total }} - |
- - |
- - {% if totales.bultos_total != 0 %}{{ totales.bultos_total }}{% endif %} - - |
-
- - {{ totales.clave_bultos or '' }} - - |
-
- {{ totales.peso_neto_total }} - |
-
- {{ totales.peso_bruto_total }} - |
- |||
|
- {{ factura.observaciones }} - |
-
-
- {{ cliente_proveedor.nombre }} - |
- ||||||||
|
- |
- |||||||||
|
+ + Observaciones: + TOTALES + + |
+
+ {{ totales.cantidad_total }} + |
+ + |
+ + {% if totales.bultos_total != 0 %}{{ totales.bultos_total }}{% endif %} + + |
+
+ + {{ totales.clave_bultos or '' }} + + |
+
+ {{ totales.peso_neto_total_lbs }} LBS +{{ totales.peso_neto_total }} KGS + |
+
+ {{ totales.peso_bruto_total_lbs }} LBS +{{ totales.peso_bruto_total }} KGS + |
+ |||
|
+ {{ factura.observaciones }} + |
+
+
+ {{ cliente_proveedor.nombre }} + |
+ ||||||||
|
+ |
+ |||||||||