Add proportional quantity calculation and caching for line items in discharge ledger processing

- Introduced `_proportional_qty` function to calculate prorated quantities based on consumed amounts.
- Implemented caching for line items and import invoice numbers to optimize database queries during discharge ledger registration.
- Enhanced `register_discharge_ledger` function to utilize cached data for improved performance and reduced N+1 query issues.
- Updated discharge detail creation to include denormalized fields for reporting, such as net and gross weights, and customs information.
This commit is contained in:
2026-03-18 08:24:46 -05:00
parent 2a861e480c
commit 420901dd4e

View File

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