From a6c5abbe4c0269a5f7cec782c03bc18836a5f367 Mon Sep 17 00:00:00 2001 From: AlexeerCT Date: Tue, 24 Mar 2026 18:52:53 -0500 Subject: [PATCH] Enhance invoice validation logic to support existing invoices - Updated the `validate_common` function to accept an optional `existing_invoice` parameter, allowing for more robust validation when updating invoices. - Introduced a new helper function, `_normalize_invoice_currency_value`, to standardize currency code handling. - Adjusted currency validation logic to ensure proper handling of existing invoice data, preventing changes to currency types when associated line items exist. - Modified the `validate_update` functions in both imports and exports to pass the `existing_invoice` parameter, ensuring consistent validation across different update scenarios. --- .../a76/invoices/common/common_validators.py | 151 +++++++++++------- .../a76/invoices/exports/validators/update.py | 4 +- .../a76/invoices/imports/validators/update.py | 4 +- 3 files changed, 99 insertions(+), 60 deletions(-) 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 70f7bb5f..3f7dc48b 100644 --- a/backend/api/v1/modules/a76/invoices/common/common_validators.py +++ b/backend/api/v1/modules/a76/invoices/common/common_validators.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Any, Dict, Optional, Union from core.exceptions import ErrorCollector from sqlalchemy import func from sqlalchemy.orm import Session @@ -17,8 +17,14 @@ from api.v1.modules.a76.manifests.manifest.models import Manifest from api.v1.modules.public.reference_data.incoterms.models import Incoterm from api.v1.modules.public.reference_data.currency_types.models import CurrencyType from api.v1.modules.public.reference_data.customs_sections.models import CustomsSection -from core.exceptions import ErrorCollector -from typing import Dict, Any + + +def _normalize_invoice_currency_value(value) -> str: + """Lowercase currency code (foreign/local/manual) for comparisons.""" + if value is None or value == "": + return "" + raw = getattr(value, "value", value) + return str(raw).lower() def invoice_exists( @@ -174,10 +180,11 @@ def validate_required_fields_by_operation( def validate_common( db: Session, - invoice: schemas.InvoiceHeaderUpdate, + invoice: Union[schemas.InvoiceHeaderCreate, schemas.InvoiceHeaderUpdate], tenant_id: int, company_id: int, errors: ErrorCollector, + existing_invoice: Optional[models.InvoiceHeader] = None, ): if invoice.compliance_mx.pedimento_id: pedimento = ( @@ -560,69 +567,97 @@ def validate_common( value=invoice.logistics.transport_num, ) - invoice.financials.currency = invoice.financials.currency or "foreign" - - if invoice.financials.currency not in [c.value for c in Currency]: - errors.add_error( - field="financials.currency", - message="La Moneda proporcionada no es válida.", - solution=[f"Selecciona una Moneda válida: {[c.value for c in Currency]}"], - code="INVALID_CURRENCY", - value=invoice.financials.currency, - ) - else: - # Only check for existing items during update operations (when invoice has an id) - if hasattr(invoice, "id"): - has_items = ( - db.query(LineItem) - .filter( - LineItem.invoice_id == invoice.id, - LineItem.tenant_id == tenant_id, - LineItem.company_id == company_id, - ) - .first() + if invoice.financials: + submitted = invoice.financials.currency + stored_str = "" + if existing_invoice and existing_invoice.financials is not None: + stored_str = _normalize_invoice_currency_value( + existing_invoice.financials.currency ) - if has_items: - errors.add_error( - field="items", - message=f"La opcion tipo de moneda {invoice.financials.currency} no puede ser modificada ya que la factura tiene items asociados.", - solution=[ - "Verifica la moneda de los items asociados a la factura." - ], - code="CURRENCY_CANNOT_BE_CHANGED", - value=invoice.financials.currency, - ) - if invoice.financials.currency == "foreign": - invoice.financials.currency_type = "USD" - elif invoice.financials.currency == "local": - invoice.financials.currency_type = "MXN" - elif invoice.financials.currency == "manual": - if not invoice.financials.currency_type: + if submitted is None or submitted == "": + if stored_str: + invoice.financials.currency = stored_str + else: + invoice.financials.currency = "foreign" + else: + invoice.financials.currency = ( + _normalize_invoice_currency_value(submitted) or "foreign" + ) + + if invoice.financials.currency not in [c.value for c in Currency]: errors.add_error( - field="financials.currency_type", - message="El Tipo de Moneda es obligatorio cuando la Moneda es 'manual'.", - solution=["Proporciona un Tipo de Moneda válido"], - code="REQUIRED_FIELD", - value=invoice.financials.currency_type, + field="financials.currency", + message="La Moneda proporcionada no es válida.", + solution=[f"Selecciona una Moneda válida: {[c.value for c in Currency]}"], + code="INVALID_CURRENCY", + value=invoice.financials.currency, ) else: - currency_exists = ( - db.query(CurrencyType) - .filter(CurrencyType.code == invoice.financials.currency_type) - .first() - ) - if not currency_exists: + invoice_id = getattr(invoice, "id", None) + if ( + invoice_id is not None + and existing_invoice is not None + and existing_invoice.financials is not None + ): + old_c = _normalize_invoice_currency_value( + existing_invoice.financials.currency + ) + new_c = invoice.financials.currency + if old_c != new_c: + has_items = ( + db.query(LineItem) + .filter( + LineItem.invoice_id == invoice_id, + LineItem.tenant_id == tenant_id, + LineItem.company_id == company_id, + ) + .first() + ) + if has_items: + errors.add_error( + field="items", + message=( + "La opción tipo de moneda no puede ser modificada " + "ya que la factura tiene partidas asociadas." + ), + solution=[ + "Verifica la moneda de las partidas asociadas a la factura." + ], + code="CURRENCY_CANNOT_BE_CHANGED", + value=invoice.financials.currency, + ) + + if invoice.financials.currency == "foreign": + invoice.financials.currency_type = "USD" + elif invoice.financials.currency == "local": + invoice.financials.currency_type = "MXN" + elif invoice.financials.currency == "manual": + if not invoice.financials.currency_type: errors.add_error( field="financials.currency_type", - message="El Tipo de Moneda no existe en el Catálogo de Tipos de Moneda.", - solution=[ - "Verifica el código del Tipo de Moneda", - "Revisa el catálogo", - ], - code="NOT_FOUND", + message="El Tipo de Moneda es obligatorio cuando la Moneda es 'manual'.", + solution=["Proporciona un Tipo de Moneda válido"], + code="REQUIRED_FIELD", value=invoice.financials.currency_type, ) + else: + currency_exists = ( + db.query(CurrencyType) + .filter(CurrencyType.code == invoice.financials.currency_type) + .first() + ) + if not currency_exists: + errors.add_error( + field="financials.currency_type", + message="El Tipo de Moneda no existe en el Catálogo de Tipos de Moneda.", + solution=[ + "Verifica el código del Tipo de Moneda", + "Revisa el catálogo", + ], + code="NOT_FOUND", + value=invoice.financials.currency_type, + ) if invoice.logistics.incoterm: incoterm_exists = ( diff --git a/backend/api/v1/modules/a76/invoices/exports/validators/update.py b/backend/api/v1/modules/a76/invoices/exports/validators/update.py index 464c04dc..7c4b5cd4 100644 --- a/backend/api/v1/modules/a76/invoices/exports/validators/update.py +++ b/backend/api/v1/modules/a76/invoices/exports/validators/update.py @@ -55,7 +55,9 @@ def validate_update( ) # Primero ejecutar validaciones comunes - validate_common(db, invoice, tenant_id, company_id, errors) + validate_common( + db, invoice, tenant_id, company_id, errors, existing_invoice=existing_invoice + ) # Mapeo de columnas CSV a campos de la factura # Siguiendo la lógica del código Clarion original diff --git a/backend/api/v1/modules/a76/invoices/imports/validators/update.py b/backend/api/v1/modules/a76/invoices/imports/validators/update.py index 606df18a..565d3722 100644 --- a/backend/api/v1/modules/a76/invoices/imports/validators/update.py +++ b/backend/api/v1/modules/a76/invoices/imports/validators/update.py @@ -55,7 +55,9 @@ def validate_update( ) # Primero ejecutar validaciones comunes - validate_common(db, invoice, tenant_id, company_id, errors) + validate_common( + db, invoice, tenant_id, company_id, errors, existing_invoice=existing_invoice + ) # Mapeo de columnas CSV a campos de la factura # Siguiendo la lógica del código Clarion original