REPORTES mexicanos y americanos de facturas corregido

This commit is contained in:
2026-04-28 12:47:32 -05:00
parent cbb31dc1c9
commit eba12a9246
6 changed files with 224 additions and 67 deletions

View File

@@ -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

View File

@@ -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]:

View File

@@ -505,9 +505,8 @@
<td class="border" rowspan="2">
<p class="tiny-bold p-t-5 center">Línea</p>
</td>
<td class="border" rowspan="2">
<p class="tiny-bold p-l-2 line-10">Número de Parte</p>
<p class="tiny-bold p-l-2 line-10">Descripción</p>
<td class="border" rowspan="2" style="width: 180pt;">
<p class="tiny-bold p-l-2 line-10">Descripción de la Mercancía</p>
</td>
<td class="border" colspan="3">
<p class="tiny-bold center line-9">Comercial</p>
@@ -554,13 +553,15 @@
<p class="mini p-t-3 center">{{ loop.index }}</p>
</td>
<td class="border">
<p class="mini p-t-1" style="font-weight: bold;">{{ partida.numero_parte }}</p>
<p class="mini">{{ partida.descripcion }}</p>
<p class="mini">Frac: {{ partida.fraccion }} / Orig: {{ partida.origen or 'MEX' }}</p>
<p class="mini">
{% if partida.advalorem %}ADV: {{ partida.advalorem }}{% endif %}
{% if partida.preferencia %} / PREF: {{ partida.preferencia }}{% endif %}
<p class="mini p-t-1">
{{ partida.descripcion }} / Origen: {{ partida.origen or 'MEX' }} / Fracción: {{ partida.fraccion }} / Preferencia: {{ partida.preferencia }} / Advalorem: {{ partida.advalorem }}
</p>
{% if partida.marca %}<p class="mini">Marca: {{ partida.marca }}</p>{% endif %}
{% if partida.modelo %}<p class="mini">Modelo: {{ partida.modelo }}</p>{% endif %}
{% if partida.series %}
<p class="mini p-t-1">- SERIE(S):</p>
<p class="mini">Serie: {{ partida.series|join(', ') }}</p>
{% endif %}
</td>
<td class="border" colspan="2">
<p class="mini p-t-2 center">{{ partida.cantidad_importacion }}</p>

View File

@@ -456,9 +456,8 @@
<td class="border" rowspan="2">
<p class="tiny-bold p-t-5 center">Line</p>
</td>
<td class="border" rowspan="2">
<p class="tiny-bold p-l-2 line-10">Part Number</p>
<p class="tiny-bold p-l-2 line-10">Description</p>
<td class="border" rowspan="2" style="width: 180pt;">
<p class="tiny-bold p-l-2 line-10">Description of raw material</p>
</td>
<td class="border" colspan="3">
<p class="tiny-bold center line-9">Commercial</p>
@@ -505,10 +504,15 @@
<p class="mini p-t-3 center">{{ loop.index }}</p>
</td>
<td class="border">
<p class="mini p-t-1" style="font-weight: bold;">{{ partida.numero_parte }}</p>
<p class="mini">{{ partida.descripcion }}</p>
<p class="mini">HTS Code: {{ partida.fraccion }} / Orig: {{ partida.origen or 'MEX' }}</p>
<p class="mini p-t-1">
{{ partida.descripcion }} / HTS Code: {{ partida.fraccion }} / Origin: {{ partida.origen or 'MEX' }} / Preference: {{ partida.preferencia }} / Advalorem: {{ partida.advalorem }}
</p>
{% if partida.marca %}<p class="mini">Brand: {{ partida.marca }}</p>{% endif %}
{% if partida.modelo %}<p class="mini">Model: {{ partida.modelo }}</p>{% endif %}
{% if partida.series %}
<p class="mini p-t-1">- SERIAL(S):</p>
<p class="mini">Serial: {{ partida.series|join(', ') }}</p>
{% endif %}
</td>
<td class="border" colspan="2">
<p class="mini p-t-2 center">{{ partida.cantidad_importacion }}</p>

View File

@@ -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(

View File

@@ -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=""
)