Enhance invoice processing logic for IMD and Mexican purchases
- Updated validation logic to restrict 'IMD' document type usage unless the invoice type is 'DEF'. - Refactored value assignment in the main processing flow to handle 'DEF' and 'MEX' invoice types with specific IVA calculations. - Added logging for invoice processing to improve traceability and debugging. These changes improve the accuracy of invoice validations and processing for specific document types.
This commit is contained in:
@@ -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.",
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user