diff --git a/backend/api/v1/modules/a76/invoices/common/common_validators.py b/backend/api/v1/modules/a76/invoices/common/common_validators.py index 5447dd48..70f7bb5f 100644 --- a/backend/api/v1/modules/a76/invoices/common/common_validators.py +++ b/backend/api/v1/modules/a76/invoices/common/common_validators.py @@ -395,7 +395,7 @@ def validate_common( value=invoice.document_type, ) else: - if invoice.document_type == "IMD": + if invoice.document_type == "IMD" and invoice.invoice_type.upper() != "DEF": errors.add_error( field="document_type", message="El Tipo de Documento no puede ser 'IMD' a menos que sea un Cambio de Régimen.", diff --git a/backend/api/v1/modules/a76/invoices/imports/process/main_process.py b/backend/api/v1/modules/a76/invoices/imports/process/main_process.py index e9cd1237..c4127ee9 100644 --- a/backend/api/v1/modules/a76/invoices/imports/process/main_process.py +++ b/backend/api/v1/modules/a76/invoices/imports/process/main_process.py @@ -25,6 +25,10 @@ from .sub_process.review_rule_octave import ( ) from ...common.process.review_uma import revisa_uma from .sub_process.assing_values import assign_values_lines, assign_values_invoice +from .sub_process.assing_values_def_mex import ( + assign_values_iva_lines, + assign_values_invoice_totals, +) from ..balance.create_balance_entries import create_balance_entries @@ -246,8 +250,14 @@ def main_process(db: Session, invoice: InvoiceHeader, tenant_id: str, company_id review_weights_lbs(db, lines, tenant_id, company_id, errors) # Paso 3: Asignación de valores por partida y totalización de factura - assign_values_lines(invoice, lines) - assign_values_invoice(invoice, lines) + # Para IMPO DEF / Compras Mexicanas se usa la versión con IVA por partida. + invoice_type = (invoice.invoice_type or "").strip().upper() + if invoice_type in {"DEF", "MEX"}: + assign_values_iva_lines(invoice, lines) + assign_values_invoice_totals(invoice, lines) + else: + assign_values_lines(invoice, lines) + assign_values_invoice(invoice, lines) # Paso 4: Validaciones per-línea octave_desc, octave_available = _validate_lines(db, invoice, lines, tenant_id, company_id, errors) @@ -284,6 +294,7 @@ def main_process(db: Session, invoice: InvoiceHeader, tenant_id: str, company_id _update_invoice_totals(invoice, lines) # Paso 8: Generar saldos en a24.balance_movement (una entrada por partida) - create_balance_entries(db, invoice, lines) + if invoice_type not in {"DEF", "MEX"}: + create_balance_entries(db, invoice, lines) db.flush() \ No newline at end of file diff --git a/backend/api/v1/modules/a76/invoices/imports/process/sub_process/assing_values_def_mex.py b/backend/api/v1/modules/a76/invoices/imports/process/sub_process/assing_values_def_mex.py new file mode 100644 index 00000000..fec9e4d6 --- /dev/null +++ b/backend/api/v1/modules/a76/invoices/imports/process/sub_process/assing_values_def_mex.py @@ -0,0 +1,159 @@ +from decimal import Decimal +from typing import List + +from api.v1.modules.a76.invoices.models import Currency, InvoiceHeader +from api.v1.modules.a76.items.models import LineItem + + +def assign_values_iva_lines( + invoice: InvoiceHeader, + lines: List[LineItem], +) -> None: + """ + ASIGNAVALORES_IVA_PARTIDA + Asigna valores por partida para IMPO DEFINITIVA / COMPRAS MEXICANAS + calculando subtotal + IVA + total en ME/MN/MC. + + Nota: + - Los contadores legacy CantRetornada/CantRetornadaTemp ya no existen. + - La trazabilidad de saldos vive en balance_movement + discharges. + """ + if not invoice.financials: + return + + currency = invoice.financials.currency + tc = Decimal(str(invoice.financials.exchange_rate or 0)) + tc_mm = Decimal(str(invoice.financials.exchange_rate_mm or 0)) + iva_factor = Decimal(str(invoice.financials.iva_factor or 0)) + + for line in lines: + fin = line.financial + qty_rec = line.quantity + if fin is None or qty_rec is None: + continue + + qty = Decimal(str(qty_rec.quantity or 0)) + capture = Decimal(str(fin.unit_cost_capture or 0)) + if qty <= 0: + continue + + if currency == Currency.FOREIGN: # ME + # ME base + fin.unit_cost_usd = capture + fin.sub_import_value_usd = capture * qty + fin.vat_usd = fin.sub_import_value_usd * iva_factor / 100 + fin.value_usd = fin.sub_import_value_usd + fin.vat_usd + + # MN converted from ME + fin.unit_cost_mxn = capture * tc + fin.sub_import_value_mxn = fin.unit_cost_mxn * qty + fin.vat_mxn = fin.sub_import_value_mxn * iva_factor / 100 + fin.value_mxn = fin.sub_import_value_mxn + fin.vat_mxn + + # MC mirrors capture currency in legacy + fin.unit_cost_mc = capture + fin.sub_import_value_mc = capture * qty + fin.vat_mc = fin.sub_import_value_mc * iva_factor / 100 + fin.value_mc = fin.sub_import_value_mc + fin.vat_mc + + elif currency == Currency.LOCAL: # MN + # MN base + fin.unit_cost_mxn = capture + fin.sub_import_value_mxn = capture * qty + fin.vat_mxn = fin.sub_import_value_mxn * iva_factor / 100 + fin.value_mxn = fin.sub_import_value_mxn + fin.vat_mxn + + # ME converted from MN + fin.unit_cost_usd = (capture / tc) if tc else Decimal(0) + fin.sub_import_value_usd = fin.unit_cost_usd * qty + fin.vat_usd = fin.sub_import_value_usd * iva_factor / 100 + fin.value_usd = fin.sub_import_value_usd + fin.vat_usd + + # MC mirrors capture currency in legacy + fin.unit_cost_mc = capture + fin.sub_import_value_mc = capture * qty + fin.vat_mc = fin.sub_import_value_mc * iva_factor / 100 + fin.value_mc = fin.sub_import_value_mc + fin.vat_mc + + elif currency == Currency.MANUAL: # MC + # ME from MC * tc_mm + fin.unit_cost_usd = capture * tc_mm + fin.sub_import_value_usd = fin.unit_cost_usd * qty + fin.vat_usd = fin.sub_import_value_usd * iva_factor / 100 + fin.value_usd = fin.sub_import_value_usd + fin.vat_usd + + # MN from ME * tc + fin.unit_cost_mxn = fin.unit_cost_usd * tc + fin.sub_import_value_mxn = fin.unit_cost_mxn * qty + fin.vat_mxn = fin.sub_import_value_mxn * iva_factor / 100 + fin.value_mxn = fin.sub_import_value_mxn + fin.vat_mxn + + # MC base + fin.unit_cost_mc = capture + fin.sub_import_value_mc = capture * qty + fin.vat_mc = fin.sub_import_value_mc * iva_factor / 100 + fin.value_mc = fin.sub_import_value_mc + fin.vat_mc + + +def assign_values_invoice_totals( + invoice: InvoiceHeader, + lines: List[LineItem], +) -> None: + """ + ASIGNAVALORES_FACTURA + Totaliza cantidades, pesos y valores/IVA en encabezado para IMPO DEF/MEX. + """ + if not invoice.financials: + return + + total_qty = Decimal(0) + total_net = Decimal(0) + total_gross = Decimal(0) + total_packages = 0 + + total_val_mn = Decimal(0) + total_val_me = Decimal(0) + total_val_mc = Decimal(0) + total_iva_mn = Decimal(0) + total_iva_me = Decimal(0) + total_iva_mc = Decimal(0) + total_sub_mn = Decimal(0) + total_sub_me = Decimal(0) + total_sub_mc = Decimal(0) + + for line in lines: + q = line.quantity + f = line.financial + if q: + total_qty += Decimal(str(q.quantity or 0)) + total_net += Decimal(str(q.net_weight or 0)) + total_gross += Decimal(str(q.gross_weight or 0)) + total_packages += int(q.package_quantity or 0) + if f: + total_val_mn += Decimal(str(f.value_mxn or 0)) + total_val_me += Decimal(str(f.value_usd or 0)) + total_val_mc += Decimal(str(f.value_mc or 0)) + total_iva_mn += Decimal(str(f.vat_mxn or 0)) + total_iva_me += Decimal(str(f.vat_usd or 0)) + total_iva_mc += Decimal(str(f.vat_mc or 0)) + total_sub_mn += Decimal(str(f.sub_import_value_mxn or 0)) + total_sub_me += Decimal(str(f.sub_import_value_usd or 0)) + total_sub_mc += Decimal(str(f.sub_import_value_mc or 0)) + + fin = invoice.financials + fin.total_quantity = float(total_qty) + fin.net_weight = float(total_net) + fin.gross_weight = float(total_gross) + fin.total_packages = total_packages + + fin.value_mn = float(total_val_mn) + fin.value_me = float(total_val_me) + fin.value_mc = float(total_val_mc) + + fin.iva_mn = float(total_iva_mn) + fin.iva_me = float(total_iva_me) + fin.iva_mc = float(total_iva_mc) + + # No existe subtotal a nivel encabezado en el modelo actual. + # Se conserva en partidas (sub_import_value_*), de donde se agrega cuando se necesite. + _ = (total_sub_mn, total_sub_me, total_sub_mc) diff --git a/backend/api/v1/modules/a76/invoices/imports/process/task.py b/backend/api/v1/modules/a76/invoices/imports/process/task.py index 349d7c2e..4ca0360f 100644 --- a/backend/api/v1/modules/a76/invoices/imports/process/task.py +++ b/backend/api/v1/modules/a76/invoices/imports/process/task.py @@ -1,3 +1,5 @@ +import logging + from celery import Task from core.celery_app import celery_app @@ -12,9 +14,15 @@ from .sub_process.review_exchange_rate import review_exchange_rate from .sub_process.review_weights import review_weights_kgs, review_weights_lbs from .sub_process.review_rule_octave import valida_imp_regla_octava, descuenta_cupo_r_octava from .sub_process.assing_values import assign_values_lines, assign_values_invoice +from .sub_process.assing_values_def_mex import ( + assign_values_iva_lines, + assign_values_invoice_totals, +) from ..balance.create_balance_entries import create_balance_entries from .main_process import _validate_sisimp_limits, _update_invoice_totals, _validate_lines +logger = logging.getLogger(__name__) + def _progress(task: Task, current: int, status: str) -> None: task.update_state(state="PROGRESS", meta={"current": current, "status": status}) @@ -64,8 +72,21 @@ def process_invoice_task(self: Task, invoice_id: int, tenant_id: str, company_id # ── Paso 4: Asignación de valores ───────────────────────────────────── _progress(self, 50, "Calculando valores por partida...") - assign_values_lines(invoice, lines) - assign_values_invoice(invoice, lines) + raw_type = invoice.invoice_type + invoice_type = (raw_type or "").strip().upper() + logger.info( + "celery import process invoice_type: invoice_id=%s raw=%r normalized=%r document_type=%r", + invoice.id, + raw_type, + invoice_type, + getattr(invoice, "document_type", None), + ) + if invoice_type in {"DEF", "MEX"}: + assign_values_iva_lines(invoice, lines) + assign_values_invoice_totals(invoice, lines) + else: + assign_values_lines(invoice, lines) + assign_values_invoice(invoice, lines) # ── Paso 5: Validaciones por partida ────────────────────────────────── _progress(self, 70, "Validando partidas...") @@ -103,7 +124,8 @@ def process_invoice_task(self: Task, invoice_id: int, tenant_id: str, company_id # ── Paso 8: Generar saldos en a24.balance_movement ─────────────────── _progress(self, 98, "Generando saldos de inventario...") - create_balance_entries(db, invoice, lines) + if invoice_type not in {"DEF", "MEX"}: + create_balance_entries(db, invoice, lines) db.flush() db.commit() diff --git a/backend/api/v1/modules/a76/invoices/imports/revert/main_process.py b/backend/api/v1/modules/a76/invoices/imports/revert/main_process.py index 819a4633..3b1824ff 100644 --- a/backend/api/v1/modules/a76/invoices/imports/revert/main_process.py +++ b/backend/api/v1/modules/a76/invoices/imports/revert/main_process.py @@ -131,6 +131,7 @@ def _reset_invoice_financials(invoice: InvoiceHeader) -> None: fin.customs_value_me = 0.0 fin.iva_mn = 0.0 fin.iva_me = 0.0 + fin.iva_mc = 0.0 invoice.status = InvoiceStatus.PENDING invoice.process_method = None diff --git a/backend/api/v1/modules/sitar/common/base_service.py b/backend/api/v1/modules/sitar/common/base_service.py index b4dbe284..e58d62b3 100644 --- a/backend/api/v1/modules/sitar/common/base_service.py +++ b/backend/api/v1/modules/sitar/common/base_service.py @@ -107,16 +107,12 @@ class SitarAPIBaseService: # DEBUG LOGGING for SITAR inspection if "fracciones" in url: import logging - logger = logging.getLogger(__name__) - logger.info(f"SITAR API Response Headers for {url}: {dict(response.headers)}") + logger = logging.getLogger(__name__) try: - data = response.json() - if isinstance(data, dict): - logger.info(f"SITAR API Response Body Keys: {list(data.keys())}") - elif isinstance(data, list) and len(data) > 0: - logger.info(f"SITAR API Response List Item Keys: {list(data[0].keys())}") + data = response.json() return data except Exception: + logger.error(f"Error parsing SITAR API Response Body: {response.text}") pass return response.json()