From 73462fcec3643cea05f3e55bf63c98a16a44f1ca Mon Sep 17 00:00:00 2001 From: AlexeerCT Date: Tue, 17 Mar 2026 19:46:09 -0500 Subject: [PATCH] Add ENTRY_VOID movement type and integrate balance entry creation in invoice processing - Introduced a new movement type 'ENTRY_VOID' to handle invoice reversals, ensuring net balance remains zero. - Updated the FaLineItem DTOs to replace 'download' with 'discharge' for consistency. - Implemented balance entry creation in the invoice processing and reverting workflows, enhancing inventory management. - Adjusted progress tracking messages to reflect the new balance entry generation steps. --- backend/api/v1/modules/a24/balance_movements/models.py | 7 +++++++ backend/api/v1/modules/a24/fa/fa_item_lines/dto.py | 6 +++--- .../modules/a76/invoices/imports/process/main_process.py | 4 ++++ .../api/v1/modules/a76/invoices/imports/process/task.py | 6 ++++++ .../modules/a76/invoices/imports/revert/main_process.py | 8 ++++++++ .../api/v1/modules/a76/invoices/imports/revert/task.py | 2 +- backend/api/v1/modules/a76/items/service.py | 2 +- 7 files changed, 30 insertions(+), 5 deletions(-) diff --git a/backend/api/v1/modules/a24/balance_movements/models.py b/backend/api/v1/modules/a24/balance_movements/models.py index a0d5d71c..2fd7fe84 100644 --- a/backend/api/v1/modules/a24/balance_movements/models.py +++ b/backend/api/v1/modules/a24/balance_movements/models.py @@ -74,6 +74,12 @@ class MovementType(str, Enum): EXPIRATION = "expiration" # Balance cancelled due to deadline REGIME_CHANGE_OUT = "regime_chg_out" # Eg. temporary → definitive (exit side) + # ── Reversal (annuls a prior ENTRY — used when un-processing an invoice) ─ + # Inserting ENTRY_VOID with the same quantity as the original ENTRY leaves + # the net balance at zero, preventing any further discharges against that + # lot. A fresh ENTRY is created when the invoice is re-processed. + ENTRY_VOID = "entry_void" + # Which movement types reduce the balance (sign = -1) NEGATIVE_MOVEMENTS = { @@ -85,6 +91,7 @@ NEGATIVE_MOVEMENTS = { MovementType.TRANSFER_OUT, MovementType.EXPIRATION, MovementType.REGIME_CHANGE_OUT, + MovementType.ENTRY_VOID, } # Which types count toward "used" (CANTUSADA in Anexo 24 report) diff --git a/backend/api/v1/modules/a24/fa/fa_item_lines/dto.py b/backend/api/v1/modules/a24/fa/fa_item_lines/dto.py index 6b4cdc52..9c464513 100644 --- a/backend/api/v1/modules/a24/fa/fa_item_lines/dto.py +++ b/backend/api/v1/modules/a24/fa/fa_item_lines/dto.py @@ -56,7 +56,7 @@ class FaLineItemCreateDTO(BaseModel): subitem_number: Optional[int] = Field(0, description="Número de subpartida") # Special flags - download: Optional[bool] = Field(None, description="Indicador de descarga") + discharge: Optional[bool] = Field(None, description="Indicador de descarga") own_equipment: Optional[bool] = Field(None, description="Equipo propio") omit_annex31: Optional[bool] = Field(None, description="Omitir en Anexo 31") @@ -108,7 +108,7 @@ class FaLineItemUpdateDTO(BaseModel): subitem_number: Optional[int] = Field(None, description="Número de subpartida") # Special flags - download: Optional[bool] = Field(None, description="Indicador de descarga") + discharge: Optional[bool] = Field(None, description="Indicador de descarga") own_equipment: Optional[bool] = Field(None, description="Equipo propio") omit_annex31: Optional[bool] = Field(None, description="Omitir en Anexo 31") @@ -156,7 +156,7 @@ class FaLineItemResponseDTO(BaseModel): subitem_number: Optional[int] = Field(None, description="Número de subpartida") # Special flags - download: Optional[bool] = Field(None, description="Indicador de descarga") + discharge: Optional[bool] = Field(None, description="Indicador de descarga") own_equipment: Optional[bool] = Field(None, description="Equipo propio") omit_annex31: Optional[bool] = Field(None, description="Omitir en Anexo 31") 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 ea28d7de..df03b322 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,7 @@ 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 ..balance.create_balance_entries import create_balance_entries @@ -282,4 +283,7 @@ def main_process(db: Session, invoice: InvoiceHeader, tenant_id: str, company_id # Paso 7: Actualizar totales, IVA e incrementables y marcar como procesada _update_invoice_totals(invoice) + # Paso 8: Generar saldos en a24.balance_movement (una entrada por partida) + 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/task.py b/backend/api/v1/modules/a76/invoices/imports/process/task.py index 67c58de8..b269e129 100644 --- a/backend/api/v1/modules/a76/invoices/imports/process/task.py +++ b/backend/api/v1/modules/a76/invoices/imports/process/task.py @@ -12,6 +12,7 @@ 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 ..balance.create_balance_entries import create_balance_entries from .main_process import _validate_sisimp_limits, _update_invoice_totals, _validate_lines @@ -99,6 +100,11 @@ def process_invoice_task(self: Task, invoice_id: int, tenant_id: str, company_id sql_errors=sql_errors, ) _update_invoice_totals(invoice) + + # ── Paso 8: Generar saldos en a24.balance_movement ─────────────────── + _progress(self, 98, "Generando saldos de inventario...") + 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 8f71e2b2..f39e51ef 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 @@ -9,6 +9,7 @@ from api.v1.modules.a76.items.models import LineItem from core.exceptions import ErrorCollector from .sub_process.review_rule_octave import borra_saldos_regla_octava +from ..balance.void_balance_entries import void_balance_entries # ───────────────────────────────────────────────────────────────────────────── @@ -231,4 +232,11 @@ def revert_process( # ── Paso 2c: UPDATE QEqiMaq ─────────────────────────────────────────────── _reset_line_quantities(lines) + # ── Paso 2d: Anular saldos en a24.balance_movement ─────────────────────── + # Inserta ENTRY_VOID por cada ENTRY abierto de esta factura, dejando el + # balance neto en 0 para que las descargas de exportación no puedan + # consumir esos lotes. El guard interno confirma que no haya consumos + # activos (ya validado arriba, pero se mantiene como doble seguro). + void_balance_entries(db, invoice) + return sql_errors diff --git a/backend/api/v1/modules/a76/invoices/imports/revert/task.py b/backend/api/v1/modules/a76/invoices/imports/revert/task.py index 039baba7..fbf2b056 100644 --- a/backend/api/v1/modules/a76/invoices/imports/revert/task.py +++ b/backend/api/v1/modules/a76/invoices/imports/revert/task.py @@ -58,7 +58,7 @@ def revert_invoice_task(self: Task, invoice_id: int, tenant_id: str, company_id: ) # ── Paso 4: Confirmar transacción ───────────────────────────────────── - _progress(self, 95, "Confirmando cambios...") + _progress(self, 95, "Anulando saldos de inventario y confirmando...") db.flush() db.commit() diff --git a/backend/api/v1/modules/a76/items/service.py b/backend/api/v1/modules/a76/items/service.py index 4f3cb2d3..fa42b169 100644 --- a/backend/api/v1/modules/a76/items/service.py +++ b/backend/api/v1/modules/a76/items/service.py @@ -166,7 +166,7 @@ class ItemService: # FA data uses line.id as primary key if line_data.fa_data: fa_dict = line_data.fa_data.model_dump( - exclude_unset=True, exclude={"line_item_id"} + exclude_unset=True, exclude={"line_item_id", "includes_subitems"} ) fa_dict.update( {"id": line.id, "tenant_id": tenant_id, "company_id": company_id}