feat: enhance invoice validation and error handling with detailed messages

This commit is contained in:
AlexeerCT
2026-01-10 23:42:05 -06:00
parent 25c51120f9
commit 887cbfa5ce
9 changed files with 255 additions and 115 deletions

View File

@@ -5,13 +5,15 @@ from api.v1.modules.a76.general_catalogs.exchange_rate.models import ExchangeRat
from api.v1.modules.a76.clients_and_providers.models import ClientProvider
from api.v1.modules.public.reference_data.incoterms.models import Incoterm
from api.v1.modules.a76.items.models import Item
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 ....models import TransportType, Currency, WeightUnit
from core.exceptions import ErrorCollector
def validate_common(
db: Session,
invoice: schemas.InvoiceHeaderCreate,
invoice: schemas.InvoiceHeaderUpdate,
tenant_id: int,
company_id: int,
errors: ErrorCollector,
@@ -154,6 +156,18 @@ def validate_common(
value=invoice.compliance_mx.remesa,
)
else:
if not invoice.compliance_mx.is_pedimento_pending:
errors.add_error(
field="compliance_mx.pedimento_id",
message="El campo Pedimento es obligatorio cuando no se indica que el Pedimento está pendiente.",
solution=[
"Proporciona un ID de Pedimento",
"Marca el campo Pedimento Pendiente",
],
code="REQUIRED_FIELD",
value=invoice.compliance_mx.pedimento_id,
)
if invoice.compliance_mx.remesa and not invoice.compliance_mx.pedimento_id:
errors.add_error(
field="compliance_mx.pedimento_id",
@@ -185,7 +199,7 @@ def validate_common(
if not exchange_rate_exists:
errors.add_error(
field="financials.exchange_rate",
message=f"No existe un Tipo de Cambio registrado para la fecha {invoice.invoice_date.date()}.",
message=f"No existe un Tipo de Cambio registrado para la fecha {invoice.invoice_date}.",
solution=["Registra el Tipo de Cambio en el catálogo correspondiente"],
code="EXCHANGE_RATE_NOT_FOUND",
value=invoice.financials.exchange_rate,
@@ -215,7 +229,7 @@ def validate_common(
provider_exists = (
db.query(ClientProvider)
.filter(
ClientProvider.id == invoice.provider_id,
ClientProvider.id == invoice.compliance_mx.provider_id,
ClientProvider.tenant_id == tenant_id,
ClientProvider.company_id == company_id,
)
@@ -223,17 +237,17 @@ def validate_common(
)
if not provider_exists:
errors.add_error(
field="provider_id",
field="compliance_mx.provider_id",
message="El Proveedor no existe en el Catálogo de Clientes y Proveedores.",
solution=["Verifica el ID del Proveedor", "Revisa el catálogo"],
code="NOT_FOUND",
value=invoice.provider_id,
value=invoice.compliance_mx.provider_id,
)
selled_to_exists = (
db.query(ClientProvider)
.filter(
ClientProvider.id == invoice.selled_to_id,
ClientProvider.id == invoice.compliance_mx.sold_to_id,
ClientProvider.tenant_id == tenant_id,
ClientProvider.company_id == company_id,
)
@@ -241,17 +255,17 @@ def validate_common(
)
if not selled_to_exists:
errors.add_error(
field="selled_to_id",
field="compliance_mx.sold_to_id",
message="El Cliente no existe en el Catálogo de Clientes y Proveedores.",
solution=["Verifica el ID del Cliente", "Revisa el catálogo"],
code="NOT_FOUND",
value=invoice.selled_to_id,
value=invoice.compliance_mx.sold_to_id,
)
shipped_to_exists = (
db.query(ClientProvider)
.filter(
ClientProvider.id == invoice.shipped_to_id,
ClientProvider.id == invoice.compliance_mx.shipped_to_id,
ClientProvider.tenant_id == tenant_id,
ClientProvider.company_id == company_id,
)
@@ -259,17 +273,17 @@ def validate_common(
)
if not shipped_to_exists:
errors.add_error(
field="shipped_to_id",
field="compliance_mx.shipped_to_id",
message="El Destinatario no existe en el Catálogo de Clientes y Proveedores.",
solution=["Verifica el ID del Destinatario", "Revisa el catálogo"],
code="NOT_FOUND",
value=invoice.shipped_to_id,
value=invoice.compliance_mx.shipped_to_id,
)
customs_broker_exists = (
db.query(ClientProvider)
.filter(
ClientProvider.id == invoice.customs_broker_id,
ClientProvider.id == invoice.compliance_mx.customs_broker_id,
ClientProvider.tenant_id == tenant_id,
ClientProvider.company_id == company_id,
)
@@ -277,11 +291,11 @@ def validate_common(
)
if not customs_broker_exists:
errors.add_error(
field="customs_broker_id",
field="compliance_mx.customs_broker_id",
message="El Agente Aduanal no existe en el Catálogo de Clientes y Proveedores.",
solution=["Verifica el ID del Agente Aduanal", "Revisa el catálogo"],
code="NOT_FOUND",
value=invoice.customs_broker_id,
value=invoice.compliance_mx.customs_broker_id,
)
if invoice.logistics.carrier_id:
@@ -314,16 +328,24 @@ def validate_common(
value=invoice.logistics.transport_type,
)
else:
if invoice.logistics.transport_type == "none" and invoice.logistics.transport_num:
if (
invoice.logistics.transport_type == "none"
and invoice.logistics.transport_num
):
errors.add_error(
field="logistics.transport_num",
message="El Número de Transporte no debe proporcionarse cuando el Tipo de Transporte es 'none'.",
solution=["Elimina el Número de Transporte o selecciona un Tipo de Transporte válido"],
solution=[
"Elimina el Número de Transporte o selecciona un Tipo de Transporte válido"
],
code="INVALID_VALUE",
value=invoice.logistics.transport_num,
)
else:
if not invoice.logistics.transport_num and invoice.logistics.transport_type != "none":
if (
not invoice.logistics.transport_num
and invoice.logistics.transport_type != "none"
):
errors.add_error(
field="logistics.transport_num",
message="El Número de Transporte es obligatorio cuando se proporciona un Tipo de Transporte distinto de 'none'.",
@@ -331,40 +353,72 @@ def validate_common(
code="REQUIRED_FIELD",
value=invoice.logistics.transport_num,
)
invoice.financials.currency = (invoice.financials.currency or "foreign")
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]}"
],
solution=[f"Selecciona una Moneda válida: {[c.value for c in Currency]}"],
code="INVALID_CURRENCY",
value=invoice.financials.currency,
)
else:
has_items = db.query(Item).filter(
Item.invoice_id == invoice.id,
Item.tenant_id == tenant_id,
Item.company_id == company_id,
).first()
if has_items:
errors.add_error(
# Only check for existing items during update operations (when invoice has an id)
if hasattr(invoice, "id"):
has_items = (
db.query(Item)
.filter(
Item.invoice_id == invoice.id,
Item.tenant_id == tenant_id,
Item.company_id == company_id,
)
.first()
)
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."],
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.logistics.incoterms:
if invoice.financials.currency == "manual":
if not invoice.financials.currency_type:
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,
)
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 = (
db.query(Incoterm)
.filter(
Incoterm.code == invoice.logistics.incoterms,
Incoterm.code == invoice.logistics.incoterm,
Incoterm.tenant_id == tenant_id,
Incoterm.company_id == company_id,
)
@@ -372,13 +426,13 @@ def validate_common(
)
if not incoterm_exists:
errors.add_error(
field="logistics.incoterms",
field="logistics.incoterm",
message="El Incoterm no existe en el Catálogo de Incoterms.",
solution=["Verifica el código del Incoterm", "Revisa el catálogo"],
code="NOT_FOUND",
value=invoice.logistics.incoterms,
value=invoice.logistics.incoterm,
)
if invoice.logistics.weight_type not in [w.value for w in WeightUnit]:
errors.add_error(
field="logistics.weight_type",
@@ -389,5 +443,20 @@ def validate_common(
code="INVALID_WEIGHT_UNIT",
value=invoice.logistics.weight_type,
)
if invoice.compliance_mx.aduana:
custom_section_exists = (
db.query(CustomsSection)
.filter(
CustomsSection.customs_code == invoice.compliance_mx.aduana,
)
.first()
)
if not custom_section_exists:
errors.add_error(
field="compliance_mx.aduana",
message="La Aduana no existe en el Catálogo de Secciones Aduaneras.",
solution=["Verifica el código de la Aduana", "Revisa el catálogo"],
code="NOT_FOUND",
value=invoice.compliance_mx.aduana,
)

View File

@@ -384,7 +384,7 @@ class InvoiceHeaderCreate(InvoiceHeaderBase):
"""Schema for creating Invoice Header with nested relations"""
compliance_mx: Optional[InvoiceComplianceMxCreate] = None
financials: Optional[InvoiceFinancialsCreate] = None
logistics: Optional[List[InvoiceLogisticsCreate]] = None
logistics: Optional[InvoiceLogisticsCreate] = None
details: Optional[List[InvoiceSalesDetailsCreate]] = None
collections: Optional[List[InvoiceCollectionsCreate]] = None
@@ -418,9 +418,10 @@ class InvoiceCollectionsUpdate(InvoiceCollectionsBase):
class InvoiceHeaderUpdate(InvoiceHeaderBase):
"""Schema for updating Invoice Header with nested relations"""
id: int
compliance_mx: Optional[InvoiceComplianceMxUpdate] = None
financials: Optional[InvoiceFinancialsUpdate] = None
logistics: Optional[List[InvoiceLogisticsUpdate]] = None
logistics: Optional[InvoiceLogisticsUpdate] = None
details: Optional[List[InvoiceSalesDetailsUpdate]] = None
collections: Optional[List[InvoiceCollectionsUpdate]] = None
@@ -477,9 +478,9 @@ class InvoiceHeaderResponse(InvoiceHeaderBase):
capture_date: datetime
compliance_mx: Optional[InvoiceComplianceMxResponse] = None
financials: Optional[InvoiceFinancialsResponse] = None
logistics: List[InvoiceLogisticsResponse] = []
details: List[InvoiceSalesDetailsResponse] = []
collections: List[InvoiceCollectionsResponse] = []
logistics: Optional[InvoiceLogisticsResponse] = []
details: Optional[InvoiceSalesDetailsResponse] = []
collections: Optional[InvoiceCollectionsResponse] = []
class Config:
from_attributes = True

View File

@@ -90,8 +90,8 @@ class InvoiceService:
errors = ErrorCollector()
# Validar si la factura ya existe
#invoice_exists(db, invoice_data.invoice_number, tenant_id, company_id, errors)
#validate_create(db, invoice_data, tenant_id, company_id, errors)
invoice_exists(db, invoice_data.invoice_number, tenant_id, company_id, errors)
validate_create(db, invoice_data, tenant_id, company_id, errors)
# Si hay errores, lanzar excepción
errors.raise_if_errors("Error al crear la factura")

View File

@@ -31,6 +31,10 @@ async def base_exception_handler(
},
)
# Log detailed errors if they exist
if hasattr(exc, "errors") and exc.errors:
logger.warning(f"Validation errors details: {exc.errors}")
return JSONResponse(
status_code=exc.status_code,
content=exc.to_dict(),