diff --git a/backend/api/v1/modules/a76/invoices/exports/process/sub_process/register_discharge_ledger.py b/backend/api/v1/modules/a76/invoices/exports/process/sub_process/register_discharge_ledger.py index 1ef7d4ee..9f86550f 100644 --- a/backend/api/v1/modules/a76/invoices/exports/process/sub_process/register_discharge_ledger.py +++ b/backend/api/v1/modules/a76/invoices/exports/process/sub_process/register_discharge_ledger.py @@ -30,6 +30,8 @@ from api.v1.modules.a24.discharges.models import ( DischargeStatus, DischargeType, ) +from api.v1.modules.a76.invoices.models import InvoiceHeader as A76InvoiceHeader +from api.v1.modules.a76.items.models import LineItem from .discharge_types import DownloadEntry, AvailableLot logger = logging.getLogger(__name__) @@ -66,6 +68,20 @@ def _proportional_value( return (consume / lot_consumed_total) * lot_value +def _proportional_qty(consume: Decimal, base_qty: Optional[Decimal], base_total: Optional[Decimal]) -> Optional[Decimal]: + """ + Proratea un valor (peso/valor) en proporción a lo consumido. + - consume: cantidad consumida del lote + - base_qty: valor total del lote (ej. peso neto total del lote) + - base_total: cantidad total del lote (ej. quantity del lote) + """ + if base_qty is None: + return None + if base_total is None or base_total <= 0: + return None + return (consume / base_total) * base_qty + + # --------------------------------------------------------------------------- # Public entry point # --------------------------------------------------------------------------- @@ -91,6 +107,11 @@ def register_discharge_ledger( op_date = _export_date(export_invoice) discharge_type = _discharge_type_for_invoice(export_invoice) + # Caches to avoid N+1 queries in loops + export_line_cache: dict[int, LineItem] = {} + import_line_cache: dict[int, LineItem] = {} + import_invoice_number_cache: dict[int, str] = {} + # ── 1. DischargeHeader ──────────────────────────────────────────────── header = DischargeHeader( tenant_id=export_invoice.tenant_id, @@ -108,6 +129,14 @@ def register_discharge_ledger( for entry in active: export_line_id: Optional[int] = entry.line_item_id + export_line_obj: Optional[LineItem] = None + if export_line_id: + export_line_obj = export_line_cache.get(export_line_id) + if export_line_obj is None: + export_line_obj = db.get(LineItem, export_line_id) + if export_line_obj is not None: + export_line_cache[export_line_id] = export_line_obj + # Only iterate lots that were actually consumed consumed_lots: List[AvailableLot] = [ lot for lot in entry.available_lots if lot.consumed_qty > Decimal(0) @@ -116,6 +145,23 @@ def register_discharge_ledger( for lot in consumed_lots: consume = lot.consumed_qty + # Load import-line object for denormalized customs/weights fields + import_line_obj = import_line_cache.get(lot.import_item_line_id) + if import_line_obj is None: + import_line_obj = db.get(LineItem, lot.import_item_line_id) + if import_line_obj is not None: + import_line_cache[lot.import_item_line_id] = import_line_obj + + # Import invoice number (for origin_import_invoice in DischargeDetail) + origin_import_invoice: Optional[str] = None + if lot.import_invoice_id: + origin_import_invoice = import_invoice_number_cache.get(lot.import_invoice_id) + if origin_import_invoice is None: + inv = db.get(A76InvoiceHeader, lot.import_invoice_id) + origin_import_invoice = inv.invoice_number if inv else None + if origin_import_invoice: + import_invoice_number_cache[lot.import_invoice_id] = origin_import_invoice + # ── 2. BalanceMovement (CONSUMPTION) ────────────────────────── # Proportional value: consume / lot_consumed_total × lot_value # lot_consumed_total == consume for single-lot entries (most cases) @@ -146,6 +192,14 @@ def register_discharge_ledger( movement.order_peps = movement.id # rule 4: monotonic # ── 3. DischargeDetail ───────────────────────────────────────── + # Denormalized fields expected by reports: + imp_cust = import_line_obj.customs if import_line_obj else None + imp_qty = import_line_obj.quantity if import_line_obj else None + imp_total_qty = imp_qty.quantity if imp_qty else None + + net_weight = _proportional_qty(consume, imp_qty.net_weight if imp_qty else None, imp_total_qty) + gross_weight = _proportional_qty(consume, imp_qty.gross_weight if imp_qty else None, imp_total_qty) + detail = DischargeDetail( tenant_id=export_invoice.tenant_id, company_id=export_invoice.company_id, @@ -157,8 +211,21 @@ def register_discharge_ledger( unit_of_measure=entry.unit_of_measure or None, value_me=value_me, value_mn=value_mn, + net_weight=net_weight, + gross_weight=gross_weight, + tariff_fraction=imp_cust.fraction if imp_cust else None, + fraction_type=imp_cust.fraction_type if imp_cust else None, + ad_valorem=imp_cust.advalorem if imp_cust else None, + country_of_origin=imp_cust.origin_country if imp_cust else None, + sector=imp_cust.sector if imp_cust else None, procedence=entry.origin_procedure or None, part_number=entry.part_number or None, + export_part_number=( + export_line_obj.part_info.part_number + if export_line_obj and export_line_obj.part_info and export_line_obj.part_info.part_number + else None + ), + origin_import_invoice=origin_import_invoice, ) db.add(detail) total_movements += 1