From eba12a9246e90796411ba41bdb840fe3831e6d98 Mon Sep 17 00:00:00 2001 From: Kevin_Ramirez Date: Tue, 28 Apr 2026 12:47:32 -0500 Subject: [PATCH] REPORTES mexicanos y americanos de facturas corregido --- .../importacion/facturas/mex/schemas.py | 4 + .../importacion/facturas/mex/service.py | 99 +++++++++++++++---- .../facturas/templates/factura_mex_ver.html | 19 ++-- .../facturas/templates/factura_usa_ver.html | 18 ++-- .../importacion/facturas/usa/service.py | 95 ++++++++++++++---- .../importacion/packing_list/service.py | 56 ++++++++--- 6 files changed, 224 insertions(+), 67 deletions(-) diff --git a/backend/api/v1/modules/a76/reports/importacion/facturas/mex/schemas.py b/backend/api/v1/modules/a76/reports/importacion/facturas/mex/schemas.py index e9a52c8a..f8822e66 100644 --- a/backend/api/v1/modules/a76/reports/importacion/facturas/mex/schemas.py +++ b/backend/api/v1/modules/a76/reports/importacion/facturas/mex/schemas.py @@ -66,6 +66,10 @@ class PartidaSchema(BaseModel): advalorem:Optional[str] = "" preferencia:Optional[str] = "" + + marca: Optional[str] = "" + modelo: Optional[str] = "" + series: List[str] = [] cantidad_importacion: Union[float, str] unidad_medida: str 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 300f462e..6aa4e0d3 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 @@ -23,6 +23,9 @@ 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.line_customs.models import LineCustom +from api.v1.modules.a76.items.line_descriptions.models import LineDescription +from api.v1.modules.a76.items.series.models import Serie # --- TRANSPORTATION MODELS --- from api.v1.modules.a76.transportation.transporters.models import Transporter @@ -454,6 +457,13 @@ class FacturaImportacionMexService: ) partidas_list = [] + # Initialize raw totals + raw_cant = 0.0 + raw_valor = 0.0 + raw_peso_n = 0.0 + raw_peso_b = 0.0 + raw_bultos = 0 + for line in lines: qty = ( db.query(LineQuantity) @@ -465,6 +475,21 @@ class FacturaImportacionMexService: .filter(LineFinancial.item_line_id == line.id) .first() ) + customs = ( + db.query(LineCustom) + .filter(LineCustom.item_line_id == line.id) + .first() + ) + desc_obj = ( + db.query(LineDescription) + .filter(LineDescription.item_line_id == line.id) + .first() + ) + series_objs = ( + db.query(Serie) + .filter(Serie.line_item_id == line.id) + .all() + ) part_master = db.query(Part).filter(Part.id == line.part_number_id).first() desc_final = "S/D" @@ -484,6 +509,10 @@ class FacturaImportacionMexService: # Fetch Origin from Master Catalog (FaPart) if part_master.fa_data and part_master.fa_data.origin_country: origen_final = part_master.fa_data.origin_country + + # Prioritize description from LineDescription (specific to this invoice) + if desc_obj: + desc_final = desc_obj.description_spanish or desc_obj.description_english or desc_final fraccion_limpia = fraccion_raw.replace(".", "").strip() if fraccion_limpia: @@ -498,21 +527,36 @@ class FacturaImportacionMexService: preferencia_txt = "General" advalorem_txt = "0%" - fraccion_imprimir = fraccion_raw - if fraccion_db: - # Si el valor en BD es None, "0", o vacío, dejarlo como "0%" o "EXENTO" + # 1. Prioridad: Datos específicos de la partida (LineCustom) + if customs: + if customs.fraction_type: + preferencia_txt = str(customs.fraction_type).upper() + + if customs.advalorem: + adv_val = customs.advalorem.strip() + if adv_val not in ["0", "0.0", "0.00", ""]: + advalorem_txt = adv_val if "%" in adv_val else f"{adv_val}%" + + # 2. Fallback: Datos de la fracción en catálogo (TariffFraction) si no hay en la partida + elif fraccion_db: adv_db = fraccion_db.adv_impo if adv_db and adv_db.strip() not in ["0", "0.0", "0.00", ""]: advalorem_txt = adv_db if "%" in adv_db else f"{adv_db}%" else: advalorem_txt = "0%" + # 3. Formatear Fracción para imprimir + if fraccion_db: fraccion_imprimir = fraccion_db.fraction or fraccion_raw else: - fraccion_imprimir = self._format_fraccion_fallback(fraccion_limpia) + # 4. Datos de Marca, Modelo y Series + marca_val = desc_obj.brand if desc_obj else "" + modelo_val = desc_obj.model if desc_obj else "" + series_list = [s.serial_numbers for s in series_objs if s.serial_numbers] + # Logic to determine values - Prioritize Specific Currency Columns v_unitario = 0.0 v_total = 0.0 @@ -544,6 +588,13 @@ class FacturaImportacionMexService: elif v_total > 0 and v_unitario == 0: v_unitario = v_total / cantidad + # Accumulate raw totals + raw_cant += float(qty.quantity) if qty and qty.quantity else 0.0 + raw_valor += v_total + raw_peso_n += float(qty.net_weight) if qty and qty.net_weight else 0.0 + raw_peso_b += float(qty.gross_weight) if qty and qty.gross_weight else 0.0 + raw_bultos += int(qty.package_quantity) if qty and qty.package_quantity else 0 + # Obtener descripción de la unidad de medida desde la tabla a76.item_lines unidad_desc = "" if line.unit_of_measure: @@ -568,6 +619,9 @@ class FacturaImportacionMexService: origen=origen_final, advalorem=advalorem_txt, preferencia=preferencia_txt, + marca=marca_val, + modelo=modelo_val, + series=series_list, cantidad_importacion=self.formatear_numero( qty.quantity if qty else 0 ), @@ -588,7 +642,13 @@ class FacturaImportacionMexService: ) totales = self.calcular_totales( - partidas_list, Decimal(factura_schema.tipo_cambio) + partidas_list, + Decimal(factura_schema.tipo_cambio), + raw_cant=raw_cant, + raw_valor=raw_valor, + raw_peso_n=raw_peso_n, + raw_peso_b=raw_peso_b, + raw_bultos=raw_bultos ) return FacturaImportacionCompleta( @@ -605,26 +665,29 @@ class FacturaImportacionMexService: raise HTTPException(status_code=500, detail=f"Error: {str(e)}") def calcular_totales( - self, partidas: List[PartidaSchema], tipo_cambio: Decimal + self, + partidas: List[PartidaSchema], + tipo_cambio: Decimal, + raw_cant: float = 0, + raw_valor: float = 0, + raw_peso_n: float = 0, + raw_peso_b: float = 0, + raw_bultos: int = 0 ) -> 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) - peso_b = sum(p.peso_bruto for p in partidas) - bultos = sum(p.cantidad_bultos for p in partidas) + # Use raw values passed from obtener_datos to avoid TypeError with formatted strings 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"): + if raw_bultos > 1 and clave_comun and not clave_comun.endswith("S"): clave_comun += "S" tc = float(tipo_cambio) if tipo_cambio else 1.0 return TotalesSchema( - cantidad_total=self.formatear_numero(cant), - bultos_total=bultos, + cantidad_total=self.formatear_numero(raw_cant), + bultos_total=raw_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), + peso_neto_total=self.formatear_numero(raw_peso_n), + peso_bruto_total=self.formatear_numero(raw_peso_b), + valor_total_total=self.formatear_numero(raw_valor), + valor_total_dolares=self.formatear_numero(raw_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]: diff --git a/backend/api/v1/modules/a76/reports/importacion/facturas/templates/factura_mex_ver.html b/backend/api/v1/modules/a76/reports/importacion/facturas/templates/factura_mex_ver.html index 215d8fa8..d78932fe 100644 --- a/backend/api/v1/modules/a76/reports/importacion/facturas/templates/factura_mex_ver.html +++ b/backend/api/v1/modules/a76/reports/importacion/facturas/templates/factura_mex_ver.html @@ -505,9 +505,8 @@

Línea

- -

Número de Parte

-

Descripción

+ +

Descripción de la Mercancía

Comercial

@@ -554,13 +553,15 @@

{{ loop.index }}

-

{{ partida.numero_parte }}

-

{{ partida.descripcion }}

-

Frac: {{ partida.fraccion }} / Orig: {{ partida.origen or 'MEX' }}

-

- {% if partida.advalorem %}ADV: {{ partida.advalorem }}{% endif %} - {% if partida.preferencia %} / PREF: {{ partida.preferencia }}{% endif %} +

+ {{ partida.descripcion }} / Origen: {{ partida.origen or 'MEX' }} / Fracción: {{ partida.fraccion }} / Preferencia: {{ partida.preferencia }} / Advalorem: {{ partida.advalorem }}

+ {% if partida.marca %}

Marca: {{ partida.marca }}

{% endif %} + {% if partida.modelo %}

Modelo: {{ partida.modelo }}

{% endif %} + {% if partida.series %} +

- SERIE(S):

+

Serie: {{ partida.series|join(', ') }}

+ {% endif %}

{{ partida.cantidad_importacion }}

diff --git a/backend/api/v1/modules/a76/reports/importacion/facturas/templates/factura_usa_ver.html b/backend/api/v1/modules/a76/reports/importacion/facturas/templates/factura_usa_ver.html index b23783cc..7ec1a681 100644 --- a/backend/api/v1/modules/a76/reports/importacion/facturas/templates/factura_usa_ver.html +++ b/backend/api/v1/modules/a76/reports/importacion/facturas/templates/factura_usa_ver.html @@ -456,9 +456,8 @@

Line

- -

Part Number

-

Description

+ +

Description of raw material

Commercial

@@ -505,10 +504,15 @@

{{ loop.index }}

-

{{ partida.numero_parte }}

-

{{ partida.descripcion }}

-

HTS Code: {{ partida.fraccion }} / Orig: {{ partida.origen or 'MEX' }}

- +

+ {{ partida.descripcion }} / HTS Code: {{ partida.fraccion }} / Origin: {{ partida.origen or 'MEX' }} / Preference: {{ partida.preferencia }} / Advalorem: {{ partida.advalorem }} +

+ {% if partida.marca %}

Brand: {{ partida.marca }}

{% endif %} + {% if partida.modelo %}

Model: {{ partida.modelo }}

{% endif %} + {% if partida.series %} +

- SERIAL(S):

+

Serial: {{ partida.series|join(', ') }}

+ {% endif %}

{{ partida.cantidad_importacion }}

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 8a4c1a91..41f38561 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 @@ -23,6 +23,9 @@ 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.line_customs.models import LineCustom +from api.v1.modules.a76.items.line_descriptions.models import LineDescription +from api.v1.modules.a76.items.series.models import Serie # --- TRANSPORTATION MODELS --- from api.v1.modules.a76.transportation.transporters.models import Transporter @@ -455,6 +458,13 @@ class FacturaImportacionUsaService: lines = db.query(LineItem).filter(LineItem.invoice_id == header.id).all() partidas_list = [] + # Initialize raw totals + raw_cant = 0.0 + raw_valor = 0.0 + raw_peso_n = 0.0 + raw_peso_b = 0.0 + raw_bultos = 0 + for line in lines: qty = ( db.query(LineQuantity) @@ -466,6 +476,21 @@ class FacturaImportacionUsaService: .filter(LineFinancial.item_line_id == line.id) .first() ) + customs = ( + db.query(LineCustom) + .filter(LineCustom.item_line_id == line.id) + .first() + ) + desc_obj = ( + db.query(LineDescription) + .filter(LineDescription.item_line_id == line.id) + .first() + ) + series_objs = ( + db.query(Serie) + .filter(Serie.line_item_id == line.id) + .all() + ) part_master = ( db.query(Part).filter(Part.id == line.part_number_id).first() ) @@ -491,6 +516,10 @@ class FacturaImportacionUsaService: if part_master.fa_data and part_master.fa_data.origin_country: origen_final = part_master.fa_data.origin_country + # Prioritize description from LineDescription (specific to this invoice) + if desc_obj: + desc_final = desc_obj.description_english or desc_obj.description_spanish or desc_final + # FRACTION LOGIC: Use US Fraction (us_fraction) if available, otherwise blank fraccion_imprimir = "" @@ -498,14 +527,25 @@ class FacturaImportacionUsaService: 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 + # Preference and Advalorem logic preferencia_txt = "General" advalorem_txt = "0%" + # 1. Prioridad: Datos específicos de la partida (LineCustom) + if customs: + if customs.fraction_type: + preferencia_txt = str(customs.fraction_type).upper() + + if customs.advalorem: + adv_val = customs.advalorem.strip() + if adv_val not in ["0", "0.0", "0.00", ""]: + advalorem_txt = adv_val if "%" in adv_val else f"{adv_val}%" + + # 2. Marca, Modelo y Series + marca_val = desc_obj.brand if desc_obj else "" + modelo_val = desc_obj.model if desc_obj else "" + series_list = [s.serial_numbers for s in series_objs if s.serial_numbers] + # 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. @@ -537,6 +577,13 @@ class FacturaImportacionUsaService: elif v_total > 0 and v_unitario == 0: v_unitario = v_total / cantidad + # Accumulate raw totals + raw_cant += float(qty.quantity) if qty and qty.quantity else 0.0 + raw_valor += v_total + raw_peso_n += float(qty.net_weight) if qty and qty.net_weight else 0.0 + raw_peso_b += float(qty.gross_weight) if qty and qty.gross_weight else 0.0 + raw_bultos += int(qty.package_quantity) if qty and qty.package_quantity else 0 + # UOM Mapping for English context uom_raw = line.unit_of_measure_info.code if line.unit_of_measure_info else "PCS" if uom_raw == "PZA": @@ -550,6 +597,9 @@ class FacturaImportacionUsaService: origen=origen_final, advalorem=advalorem_txt, preferencia=preferencia_txt, + marca=marca_val, + modelo=modelo_val, + series=series_list, cantidad_importacion=self.formatear_numero( qty.quantity if qty else 0 ), @@ -572,7 +622,13 @@ class FacturaImportacionUsaService: ) totales = self.calcular_totales( - partidas_list, Decimal(factura_schema.tipo_cambio) + partidas_list, + Decimal(factura_schema.tipo_cambio), + raw_cant=raw_cant, + raw_valor=raw_valor, + raw_peso_n=raw_peso_n, + raw_peso_b=raw_peso_b, + raw_bultos=raw_bultos ) return FacturaImportacionCompleta( @@ -589,13 +645,16 @@ class FacturaImportacionUsaService: raise HTTPException(status_code=500, detail=f"Error: {str(e)}") def calcular_totales( - self, partidas: List[PartidaSchema], tipo_cambio: Decimal + self, + partidas: List[PartidaSchema], + tipo_cambio: Decimal, + raw_cant: float = 0, + raw_valor: float = 0, + raw_peso_n: float = 0, + raw_peso_b: float = 0, + raw_bultos: int = 0 ) -> 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) - peso_b = sum(p.peso_bruto for p in partidas) - bultos = sum(p.cantidad_bultos for p in partidas) + # Use raw values passed from obtener_datos to avoid TypeError with formatted strings 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" @@ -603,13 +662,13 @@ class FacturaImportacionUsaService: tc = float(tipo_cambio) if tipo_cambio else 1.0 return TotalesSchema( - cantidad_total=self.formatear_numero(cant), - bultos_total=bultos, + cantidad_total=self.formatear_numero(raw_cant), + bultos_total=raw_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), + peso_neto_total=self.formatear_numero(raw_peso_n), + peso_bruto_total=self.formatear_numero(raw_peso_b), + valor_total_total=self.formatear_numero(raw_valor), + valor_total_dolares=self.formatear_numero(raw_valor / tc if tc > 0 else 0), ) def generar_factura_completa( 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 822294ae..6ca748f3 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 @@ -280,6 +280,14 @@ class PackingListService: lines = db.query(LineItem).filter(LineItem.invoice_id == header.id).all() partidas_list = [] + # Initialize raw totals + raw_cant = 0.0 + raw_peso_n = 0.0 + raw_peso_b = 0.0 + raw_peso_n_lb = 0.0 + raw_peso_b_lb = 0.0 + raw_bultos = 0 + for line in lines: 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() @@ -306,6 +314,14 @@ class PackingListService: peso_bruto_lb = raw_gross * 2.20462 # -------------------------------- + # Accumulate raw totals + raw_cant += float(qty.quantity) if qty and qty.quantity else 0.0 + raw_peso_n += peso_neto_kg + raw_peso_b += peso_bruto_kg + raw_peso_n_lb += peso_neto_lb + raw_peso_b_lb += peso_bruto_lb + raw_bultos += int(qty.package_quantity) if qty and qty.package_quantity else 0 + custom_obj = db.query(LineCustom).filter(LineCustom.item_line_id == line.id).first() part_master = db.query(Part).filter(Part.id == line.part_number_id).first() @@ -361,7 +377,16 @@ class PackingListService: valor_total=v_total # Hidden )) - totales = self.calcular_totales(partidas_list, Decimal(factura_schema.tipo_cambio)) + totales = self.calcular_totales( + partidas_list, + Decimal(factura_schema.tipo_cambio), + raw_cant=raw_cant, + raw_peso_n=raw_peso_n, + raw_peso_b=raw_peso_b, + raw_peso_n_lbs=raw_peso_n_lb, + raw_peso_b_lbs=raw_peso_b_lb, + raw_bultos=raw_bultos + ) return PackingListSchema( cliente_proveedor=cliente_proveedor, cliente_vendido=cliente_vendido, @@ -376,24 +401,25 @@ class PackingListService: print(f"Error Service A76: {e}") raise HTTPException(status_code=500, detail=f"Error: {str(e)}") - def calcular_totales(self, partidas: List[PartidaSchema], tipo_cambio: Decimal) -> TotalesSchema: - cant = sum(float(p.cantidad_importacion) for p in partidas) - # 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) - + def calcular_totales(self, + partidas: List[PartidaSchema], + tipo_cambio: Decimal, + raw_cant: float = 0, + raw_peso_n: float = 0, + raw_peso_b: float = 0, + raw_peso_n_lbs: float = 0, + raw_peso_b_lbs: float = 0, + raw_bultos: int = 0 + ) -> TotalesSchema: + # Use raw values to avoid ValueError/TypeError with formatted strings 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 raw_bultos > 1 and clave_comun and not clave_comun.endswith("S"): clave_comun += "S" 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), + cantidad_total=self.formatear_numero(raw_cant), bultos_total=raw_bultos, clave_bultos=clave_comun, + peso_neto_total=self.formatear_numero(raw_peso_n), peso_bruto_total=self.formatear_numero(raw_peso_b), + peso_neto_total_lbs=self.formatear_numero(raw_peso_n_lbs), peso_bruto_total_lbs=self.formatear_numero(raw_peso_b_lbs), valor_total_total="", valor_total_dolares="" )