Merge pull request 'fix/facturas_ame_mez' (#358) from fix/facturas_ame_mez into development
Reviewed-on: ADUANASOFT/anexo76#358
This commit is contained in:
@@ -9,28 +9,41 @@ from .. import schemas
|
||||
def apply_calculations(
|
||||
invoice: schemas.InvoiceHeaderUpdate,
|
||||
):
|
||||
if invoice.invoice_type == "CR":
|
||||
invoice.compliance_mx.is_regime_change = True
|
||||
else:
|
||||
invoice.compliance_mx.is_regime_change = False
|
||||
|
||||
increments_me = (invoice.financials.freight or 0) + (invoice.financials.insurance or 0) + (invoice.financials.packaging or 0) + (invoice.financials.other_increments or 0)
|
||||
if invoice.financials.currency == "foreign":
|
||||
invoice.financials.total_increments_me = increments_me
|
||||
invoice.financials.total_increments_mn = invoice.financials.total_increments_me * invoice.financials.exchange_rate
|
||||
invoice.financials.currency_type = "USD"
|
||||
elif invoice.financials.currency == "local":
|
||||
invoice.financials.total_increments_mn = increments_me
|
||||
invoice.financials.total_increments_me = invoice.financials.total_increments_mn / invoice.financials.exchange_rate
|
||||
invoice.financials.currency_type = "MXN"
|
||||
elif invoice.financials.currency == "manual":
|
||||
invoice.financials.total_increments_me = (increments_me)/invoice.financials.exchange_rate
|
||||
invoice.financials.total_increments_mn = invoice.financials.total_increments_me * invoice.financials.exchange_rate
|
||||
|
||||
if invoice.compliance_mx:
|
||||
if invoice.invoice_type == "CR":
|
||||
invoice.compliance_mx.is_regime_change = True
|
||||
else:
|
||||
invoice.compliance_mx.is_regime_change = False
|
||||
|
||||
invoice.compliance_mx.is_pedimento_pending = False
|
||||
if not invoice.compliance_mx.pedimento_id:
|
||||
invoice.compliance_mx.is_pedimento_pending = True
|
||||
if invoice.financials:
|
||||
# Arithmetica defensiva: (val or 0)
|
||||
freight = invoice.financials.freight or 0
|
||||
insurance = invoice.financials.insurance or 0
|
||||
packaging = invoice.financials.packaging or 0
|
||||
other = invoice.financials.other_increments or 0
|
||||
increments_me = freight + insurance + packaging + other
|
||||
|
||||
# Tipo de cambio seguro
|
||||
tc = invoice.financials.exchange_rate or 1 # Fallback a 1 para evitar division por cero
|
||||
|
||||
if invoice.financials.currency == "foreign":
|
||||
invoice.financials.total_increments_me = increments_me
|
||||
invoice.financials.total_increments_mn = increments_me * tc
|
||||
invoice.financials.currency_type = "USD"
|
||||
elif invoice.financials.currency == "local":
|
||||
invoice.financials.total_increments_mn = increments_me
|
||||
invoice.financials.total_increments_me = increments_me / tc if tc != 0 else 0
|
||||
invoice.financials.currency_type = "MXN"
|
||||
elif invoice.financials.currency == "manual":
|
||||
# TC_MM para moneda manual
|
||||
tc_mm = invoice.financials.exchange_rate_mm or 1
|
||||
invoice.financials.total_increments_me = increments_me / tc_mm if tc_mm != 0 else 0
|
||||
invoice.financials.total_increments_mn = invoice.financials.total_increments_me * tc
|
||||
|
||||
if invoice.compliance_mx:
|
||||
invoice.compliance_mx.is_pedimento_pending = False
|
||||
if not invoice.compliance_mx.pedimento_id:
|
||||
invoice.compliance_mx.is_pedimento_pending = True
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ def clean_dict(data_dict: dict) -> dict:
|
||||
if isinstance(value, str) and not value.strip():
|
||||
cleaned[key] = None
|
||||
|
||||
elif value == 0 and (key.endswith('_id') or key == 'remesa'):
|
||||
cleaned[key] = None
|
||||
else:
|
||||
cleaned[key] = value
|
||||
return cleaned
|
||||
|
||||
@@ -14,7 +14,7 @@ def validate_create(db: Session, invoice: InvoiceHeaderCreate, tenant_id: int, c
|
||||
if not invoice.invoice_type:
|
||||
errors.add_required_error("invoice_type")
|
||||
|
||||
if not invoice.document_type and invoice.invoice_type != "MEX":
|
||||
if not invoice.document_type and invoice.invoice_type not in ["MEX", "AME"]:
|
||||
errors.add_required_error("document_type")
|
||||
|
||||
if not invoice.invoice_number:
|
||||
|
||||
@@ -235,7 +235,7 @@ def validate_update(
|
||||
invoice.compliance_mx.aduana = clean_str(invoice.compliance_mx.aduana)
|
||||
|
||||
# Validar que aduana sea obligatorio (excepto para MEX)
|
||||
if existing_invoice.invoice_type != "MEX":
|
||||
if existing_invoice.invoice_type not in ["MEX", "AME"]:
|
||||
current_aduana = invoice.compliance_mx.aduana if invoice.compliance_mx else (existing_invoice.compliance_mx.aduana if existing_invoice.compliance_mx else None)
|
||||
if not current_aduana:
|
||||
errors.add_required_error("aduana")
|
||||
|
||||
@@ -511,7 +511,7 @@ class InvoiceHeaderUpdate(InvoiceHeaderBase):
|
||||
operation_type: Optional[OperationType] = None
|
||||
|
||||
compliance_mx: Optional[InvoiceComplianceMxUpdate] = None
|
||||
financials: Optional[InvoiceFinancialsUpdate]
|
||||
financials: Optional[InvoiceFinancialsUpdate] = None
|
||||
logistics: Optional[InvoiceLogisticsUpdate] = None
|
||||
details: Optional[List[InvoiceSalesDetailsUpdate]] = None
|
||||
collections: Optional[List[InvoiceCollectionsUpdate]] = None
|
||||
|
||||
@@ -405,6 +405,10 @@ class InvoiceService:
|
||||
# Autocalculo remesa (si aplica) ANTES de validar
|
||||
_autofill_remesa_if_needed(db, invoice_data, tenant_id, company_id)
|
||||
|
||||
# DEBUG: Log payload for analysis
|
||||
print(f"DEBUG: Creating invoice {invoice_data.invoice_number} of type {invoice_data.invoice_type}")
|
||||
print(f"DEBUG: Payload: {invoice_data.model_dump()}")
|
||||
|
||||
# Validar si la factura ya existe
|
||||
invoice_exists(db, invoice_data.invoice_number, tenant_id, company_id, errors)
|
||||
if invoice_data.operation_type == "exp":
|
||||
@@ -443,8 +447,8 @@ class InvoiceService:
|
||||
invoice_dict["capture_user"] = username
|
||||
invoice_dict["who_processed"] = username
|
||||
|
||||
# Ensure document_type respects DB constraints for MEX invoices (bypass clean_dict)
|
||||
if invoice_dict.get("invoice_type") == "MEX" and not invoice_dict.get("document_type"):
|
||||
# Ensure document_type respects DB constraints for MEX/AME invoices (bypass clean_dict)
|
||||
if invoice_dict.get("invoice_type") in ["MEX", "AME"] and not invoice_dict.get("document_type"):
|
||||
invoice_dict["document_type"] = None
|
||||
|
||||
new_invoice = models.InvoiceHeader(**invoice_dict)
|
||||
@@ -536,6 +540,10 @@ class InvoiceService:
|
||||
company_id: int,
|
||||
) -> Optional[models.InvoiceHeader]:
|
||||
"""Update an existing invoice with validation"""
|
||||
|
||||
# DEBUG: Log payload for analysis
|
||||
print(f"DEBUG: Updating invoice ID {invoice_id} of type {invoice_data.invoice_type}")
|
||||
print(f"DEBUG: Payload: {invoice_data.model_dump(exclude_unset=True)}")
|
||||
|
||||
# Validaciones con ErrorCollector
|
||||
errors = ErrorCollector()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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=""
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user