feat(invoice): enhance invoice update validation and payload construction
This commit is contained in:
@@ -12,7 +12,8 @@ class FaLineItemCreateDTO(BaseModel):
|
|||||||
"""DTO para crear una línea de activo fijo"""
|
"""DTO para crear una línea de activo fijo"""
|
||||||
|
|
||||||
# line_item_id references the id in a76.item_lines
|
# line_item_id references the id in a76.item_lines
|
||||||
line_item_id: int = Field(..., description="ID de la línea base en a76.item_lines")
|
# Not required on creation - will be set when the LineItem is created
|
||||||
|
line_item_id: Optional[int] = Field(None, description="ID de la línea base en a76.item_lines")
|
||||||
|
|
||||||
# Asset information (SCAF specific)
|
# Asset information (SCAF specific)
|
||||||
asset_number: Optional[str] = Field(
|
asset_number: Optional[str] = Field(
|
||||||
|
|||||||
@@ -1,2 +1,231 @@
|
|||||||
def validate_update():
|
from typing import Optional
|
||||||
pass
|
from datetime import date
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from core.exceptions import ErrorCollector
|
||||||
|
from ....schemas import InvoiceHeaderUpdate
|
||||||
|
from ....models import InvoiceHeader
|
||||||
|
|
||||||
|
|
||||||
|
# Helper function para limpiar strings (equivalente a Clip())
|
||||||
|
def clean_str(value: Optional[str]) -> Optional[str]:
|
||||||
|
if value is None or value == "":
|
||||||
|
return None
|
||||||
|
return value.strip()
|
||||||
|
|
||||||
|
def validate_update(
|
||||||
|
invoice_data: InvoiceHeaderUpdate,
|
||||||
|
existing_invoice: InvoiceHeader,
|
||||||
|
errors: ErrorCollector,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Valida y procesa la actualización parcial de una factura de importación temporal.
|
||||||
|
|
||||||
|
Lógica: Si un campo viene con valor, se limpia/valida.
|
||||||
|
Si no, se mantiene el valor existente de la factura.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
invoice_data: Datos de la factura a validar/actualizar (modificado in-place)
|
||||||
|
existing_invoice: Factura existente en la base de datos
|
||||||
|
errors: Colector de errores
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None (modifica invoice_data in-place y acumula errores en errors)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Primero ejecutar validaciones comunes
|
||||||
|
# validate_common(invoice_data, errors)
|
||||||
|
|
||||||
|
# Mapeo de columnas CSV a campos de la factura
|
||||||
|
# Siguiendo la lógica del código Clarion original
|
||||||
|
|
||||||
|
# Columna A: Pedimento (si no viene en CSV, usar el existente)
|
||||||
|
if invoice_data.compliance_mx.pedimento_id:
|
||||||
|
invoice_data.compliance_mx.pedimento_id = clean_str(invoice_data.compliance_mx.pedimento_id)
|
||||||
|
else:
|
||||||
|
invoice_data.compliance_mx.pedimento_id = existing_invoice.compliance_mx.pedimento_id if existing_invoice.compliance_mx else None
|
||||||
|
|
||||||
|
# Columna B: Remesa
|
||||||
|
if invoice_data.compliance_mx.remesa:
|
||||||
|
invoice_data.compliance_mx.remesa = clean_str(invoice_data.compliance_mx.remesa)
|
||||||
|
else:
|
||||||
|
invoice_data.compliance_mx.remesa = existing_invoice.compliance_mx.remesa if existing_invoice.compliance_mx else None
|
||||||
|
|
||||||
|
# Columna C: Factura (OBLIGATORIO)
|
||||||
|
invoice_data.invoice_number = clean_str(invoice_data.invoice_number)
|
||||||
|
if not invoice_data.invoice_number:
|
||||||
|
errors.add_required_error("invoice_number")
|
||||||
|
|
||||||
|
# Columna D: Fecha
|
||||||
|
if invoice_data.invoice_date:
|
||||||
|
invoice_data.invoice_date = invoice_data.invoice_date
|
||||||
|
else:
|
||||||
|
invoice_data.invoice_date = existing_invoice.invoice_date
|
||||||
|
|
||||||
|
# Columna E: Tipo Cambio
|
||||||
|
if invoice_data.financials and invoice_data.financials.exchange_rate is not None:
|
||||||
|
invoice_data.financials.exchange_rate = invoice_data.financials.exchange_rate
|
||||||
|
else:
|
||||||
|
if existing_invoice.financials:
|
||||||
|
invoice_data.financials.exchange_rate = existing_invoice.financials.exchange_rate
|
||||||
|
|
||||||
|
# Columna F: Régimen
|
||||||
|
if invoice_data.document_type:
|
||||||
|
invoice_data.document_type = clean_str(invoice_data.document_type).upper()
|
||||||
|
else:
|
||||||
|
invoice_data.document_type = existing_invoice.document_type
|
||||||
|
|
||||||
|
# Columna G: Clave Proveedor
|
||||||
|
if invoice_data.compliance_mx and invoice_data.compliance_mx.provider_id is not None:
|
||||||
|
invoice_data.compliance_mx.provider_id = invoice_data.compliance_mx.provider_id
|
||||||
|
else:
|
||||||
|
invoice_data.compliance_mx.provider_id = existing_invoice.compliance_mx.provider_id if existing_invoice.compliance_mx else None
|
||||||
|
|
||||||
|
# Columna H: Clave Vendido A
|
||||||
|
if invoice_data.compliance_mx and invoice_data.compliance_mx.sold_to_id is not None:
|
||||||
|
invoice_data.compliance_mx.sold_to_id = invoice_data.compliance_mx.sold_to_id
|
||||||
|
else:
|
||||||
|
invoice_data.compliance_mx.sold_to_id = existing_invoice.compliance_mx.sold_to_id if existing_invoice.compliance_mx else None
|
||||||
|
|
||||||
|
# Columna I: Clave Enviado A
|
||||||
|
if invoice_data.compliance_mx and invoice_data.compliance_mx.shipped_to_id is not None:
|
||||||
|
invoice_data.compliance_mx.shipped_to_id = invoice_data.compliance_mx.shipped_to_id
|
||||||
|
else:
|
||||||
|
invoice_data.compliance_mx.shipped_to_id = existing_invoice.compliance_mx.shipped_to_id if existing_invoice.compliance_mx else None
|
||||||
|
|
||||||
|
# Columna J: Clave A. Aduanal
|
||||||
|
if invoice_data.compliance_mx and invoice_data.compliance_mx.customs_broker_id is not None:
|
||||||
|
invoice_data.compliance_mx.customs_broker_id = invoice_data.compliance_mx.customs_broker_id
|
||||||
|
else:
|
||||||
|
invoice_data.compliance_mx.customs_broker_id = existing_invoice.compliance_mx.customs_broker_id if existing_invoice.compliance_mx else None
|
||||||
|
|
||||||
|
# Columna K: Clave Transportista
|
||||||
|
if invoice_data.logistics and invoice_data.logistics.carrier_id is not None:
|
||||||
|
invoice_data.logistics.carrier_id = invoice_data.logistics.carrier_id
|
||||||
|
else:
|
||||||
|
invoice_data.logistics.carrier_id = existing_invoice.logistics.carrier_id if existing_invoice.logistics else None
|
||||||
|
|
||||||
|
# Columna L: Nombre Conductor
|
||||||
|
if invoice_data.logistics and invoice_data.logistics.driver_name:
|
||||||
|
invoice_data.logistics.driver_name = clean_str(invoice_data.logistics.driver_name)
|
||||||
|
else:
|
||||||
|
invoice_data.logistics.driver_name = existing_invoice.logistics.driver_name if existing_invoice.logistics else None
|
||||||
|
|
||||||
|
# Columna M: Tipo Transporte
|
||||||
|
if invoice_data.logistics and invoice_data.logistics.transport_type:
|
||||||
|
invoice_data.logistics.transport_type = clean_str(invoice_data.logistics.transport_type)
|
||||||
|
else:
|
||||||
|
invoice_data.logistics.transport_type = existing_invoice.logistics.transport_type if existing_invoice.logistics else None
|
||||||
|
|
||||||
|
# Columna N: Número de Transporte
|
||||||
|
if invoice_data.logistics and invoice_data.logistics.transport_num:
|
||||||
|
invoice_data.logistics.transport_num = clean_str(invoice_data.logistics.transport_num)
|
||||||
|
else:
|
||||||
|
invoice_data.logistics.transport_num = existing_invoice.logistics.transport_num if existing_invoice.logistics else None
|
||||||
|
|
||||||
|
# Columna O: Tipo de Moneda
|
||||||
|
if invoice_data.financials and invoice_data.financials.currency:
|
||||||
|
invoice_data.financials.currency = clean_str(invoice_data.financials.currency).lower()
|
||||||
|
else:
|
||||||
|
invoice_data.financials.currency = existing_invoice.financials.currency if existing_invoice.financials else None
|
||||||
|
|
||||||
|
# Columna P: Clave Moneda
|
||||||
|
if invoice_data.financials and invoice_data.financials.currency_type:
|
||||||
|
invoice_data.financials.currency_type = clean_str(invoice_data.financials.currency_type).upper()
|
||||||
|
else:
|
||||||
|
invoice_data.financials.currency_type = existing_invoice.financials.currency_type if existing_invoice.financials else None
|
||||||
|
|
||||||
|
# Columna Q: Flete
|
||||||
|
if invoice_data.financials and invoice_data.financials.freight is not None:
|
||||||
|
invoice_data.financials.freight = invoice_data.financials.freight
|
||||||
|
else:
|
||||||
|
invoice_data.financials.freight = existing_invoice.financials.freight if existing_invoice.financials else None
|
||||||
|
|
||||||
|
# Columna R: Val Seguros
|
||||||
|
if invoice_data.financials and invoice_data.financials.insurance_value is not None:
|
||||||
|
invoice_data.financials.insurance_value = invoice_data.financials.insurance_value
|
||||||
|
else:
|
||||||
|
invoice_data.financials.insurance_value = existing_invoice.financials.insurance_value if existing_invoice.financials else None
|
||||||
|
|
||||||
|
# Columna S: Seguros
|
||||||
|
if invoice_data.financials and invoice_data.financials.insurance is not None:
|
||||||
|
invoice_data.financials.insurance = invoice_data.financials.insurance
|
||||||
|
else:
|
||||||
|
invoice_data.financials.insurance = existing_invoice.financials.insurance if existing_invoice.financials else None
|
||||||
|
|
||||||
|
# Columna T: Embalaje
|
||||||
|
if invoice_data.financials and invoice_data.financials.packaging is not None:
|
||||||
|
invoice_data.financials.packaging = invoice_data.financials.packaging
|
||||||
|
else:
|
||||||
|
invoice_data.financials.packaging = existing_invoice.financials.packaging if existing_invoice.financials else None
|
||||||
|
|
||||||
|
# Columna U: Otros Incrementables
|
||||||
|
if invoice_data.financials and invoice_data.financials.other_increments is not None:
|
||||||
|
invoice_data.financials.other_increments = invoice_data.financials.other_increments
|
||||||
|
else:
|
||||||
|
invoice_data.financials.other_increments = existing_invoice.financials.other_increments if existing_invoice.financials else None
|
||||||
|
|
||||||
|
# Columna V: Incoterms
|
||||||
|
if invoice_data.logistics and invoice_data.logistics.incoterm:
|
||||||
|
invoice_data.logistics.incoterm = clean_str(invoice_data.logistics.incoterm).upper()
|
||||||
|
else:
|
||||||
|
invoice_data.logistics.incoterm = existing_invoice.logistics.incoterm if existing_invoice.logistics else None
|
||||||
|
|
||||||
|
# Columna W: Precinto
|
||||||
|
if invoice_data.logistics and invoice_data.logistics.seal_number:
|
||||||
|
invoice_data.logistics.seal_number = clean_str(invoice_data.logistics.seal_number)
|
||||||
|
else:
|
||||||
|
invoice_data.logistics.seal_number = existing_invoice.logistics.seal_number if existing_invoice.logistics else None
|
||||||
|
|
||||||
|
# Columna X: Fecha de Emisión
|
||||||
|
if invoice_data.emission_date:
|
||||||
|
invoice_data.emission_date = invoice_data.emission_date
|
||||||
|
else:
|
||||||
|
invoice_data.emission_date = existing_invoice.emission_date
|
||||||
|
|
||||||
|
# Columna Y: Tipo de Peso (Opcional)
|
||||||
|
if invoice_data.logistics and invoice_data.logistics.weight_type:
|
||||||
|
invoice_data.logistics.weight_type = clean_str(invoice_data.logistics.weight_type).upper()
|
||||||
|
else:
|
||||||
|
invoice_data.logistics.weight_type = existing_invoice.logistics.weight_type if existing_invoice.logistics else None
|
||||||
|
|
||||||
|
# Columna Z: E-Document (Opcional)
|
||||||
|
if invoice_data.compliance_mx and invoice_data.compliance_mx.edocument:
|
||||||
|
invoice_data.compliance_mx.edocument = clean_str(invoice_data.compliance_mx.edocument)
|
||||||
|
else:
|
||||||
|
invoice_data.compliance_mx.edocument = existing_invoice.compliance_mx.edocument if existing_invoice.compliance_mx else None
|
||||||
|
|
||||||
|
# Columna AA: Num. Operación (Opcional)
|
||||||
|
if invoice_data.compliance_mx and invoice_data.compliance_mx.vucem_operation_num:
|
||||||
|
invoice_data.compliance_mx.vucem_operation_num = clean_str(invoice_data.compliance_mx.vucem_operation_num)
|
||||||
|
else:
|
||||||
|
invoice_data.compliance_mx.vucem_operation_num = existing_invoice.compliance_mx.vucem_operation_num if existing_invoice.compliance_mx else None
|
||||||
|
|
||||||
|
# Columna AB: Aduana (OBLIGATORIO)
|
||||||
|
if invoice_data.compliance_mx and invoice_data.compliance_mx.aduana:
|
||||||
|
invoice_data.compliance_mx.aduana = clean_str(invoice_data.compliance_mx.aduana)
|
||||||
|
else:
|
||||||
|
invoice_data.compliance_mx.aduana = existing_invoice.compliance_mx.aduana if existing_invoice.compliance_mx else None
|
||||||
|
|
||||||
|
# Validar que aduana sea obligatorio
|
||||||
|
if not invoice_data.compliance_mx or not invoice_data.compliance_mx.aduana:
|
||||||
|
errors.add_required_error("aduana")
|
||||||
|
|
||||||
|
# Columna AC: Sección de Despacho / Puerto de Entrada (Opcional)
|
||||||
|
if invoice_data.compliance_mx and invoice_data.compliance_mx.port_of_entry:
|
||||||
|
invoice_data.compliance_mx.port_of_entry = clean_str(invoice_data.compliance_mx.port_of_entry)
|
||||||
|
else:
|
||||||
|
invoice_data.compliance_mx.port_of_entry = existing_invoice.compliance_mx.port_of_entry if existing_invoice.compliance_mx else None
|
||||||
|
|
||||||
|
# Columna AD: Observación en Español (Opcional)
|
||||||
|
if invoice_data.observation_es:
|
||||||
|
invoice_data.observation_es = clean_str(invoice_data.observation_es)
|
||||||
|
else:
|
||||||
|
invoice_data.observation_es = existing_invoice.observation_es
|
||||||
|
|
||||||
|
# Columna AD: Observación en Inglés (Opcional)
|
||||||
|
if invoice_data.observation_en:
|
||||||
|
invoice_data.observation_en = clean_str(invoice_data.observation_en)
|
||||||
|
else:
|
||||||
|
invoice_data.observation_en = existing_invoice.observation_en
|
||||||
|
|
||||||
|
|||||||
@@ -201,11 +201,40 @@ class InvoiceService:
|
|||||||
invoice_data: schemas.InvoiceHeaderUpdate,
|
invoice_data: schemas.InvoiceHeaderUpdate,
|
||||||
company_id: int,
|
company_id: int,
|
||||||
) -> Optional[models.InvoiceHeader]:
|
) -> Optional[models.InvoiceHeader]:
|
||||||
# ... (El resto de tu código update se queda igual) ...
|
"""Update an existing invoice with validation"""
|
||||||
# (Te recomiendo implementar clean_dict aquí también si tienes problemas al editar)
|
|
||||||
|
# Validaciones con ErrorCollector
|
||||||
|
errors = ErrorCollector()
|
||||||
|
|
||||||
|
# Obtener la factura existente
|
||||||
invoice = InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
|
invoice = InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
|
||||||
if not invoice:
|
if not invoice:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Si se cambió el número de factura, validar que no exista otra con ese número
|
||||||
|
if invoice_data.invoice_number and invoice_data.invoice_number != invoice.invoice_number:
|
||||||
|
# Verificar que no exista otra factura con el nuevo número
|
||||||
|
existing_invoice = (
|
||||||
|
db.query(models.InvoiceHeader.id)
|
||||||
|
.filter(
|
||||||
|
models.InvoiceHeader.invoice_number == invoice_data.invoice_number,
|
||||||
|
models.InvoiceHeader.tenant_id == tenant_id,
|
||||||
|
models.InvoiceHeader.company_id == company_id,
|
||||||
|
models.InvoiceHeader.id != invoice_id, # Excluir la factura actual
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if existing_invoice:
|
||||||
|
errors.add_duplicate_error(
|
||||||
|
"invoice_number",
|
||||||
|
invoice_data.invoice_number,
|
||||||
|
f"Ya existe otra factura con el número '{invoice_data.invoice_number}'",
|
||||||
|
)
|
||||||
|
validate_update(invoice_data, invoice, errors)
|
||||||
|
|
||||||
|
# Si hay errores, lanzar excepción ANTES de actualizar
|
||||||
|
errors.raise_if_errors("Error al actualizar la factura")
|
||||||
|
|
||||||
# Update main invoice header fields
|
# Update main invoice header fields
|
||||||
update_dict = invoice_data.model_dump(
|
update_dict = invoice_data.model_dump(
|
||||||
|
|||||||
@@ -138,10 +138,11 @@ function buildInvoicePayload(formData: FormDataSet): CreateInvoiceData | UpdateI
|
|||||||
othersFormData?.is_mixed || othersFormData?.contingency_mode ||
|
othersFormData?.is_mixed || othersFormData?.contingency_mode ||
|
||||||
othersFormData?.cove || othersFormData?.operation_num ||
|
othersFormData?.cove || othersFormData?.operation_num ||
|
||||||
othersFormData?.adendas || othersFormData?.certified_number ||
|
othersFormData?.adendas || othersFormData?.certified_number ||
|
||||||
othersFormData?.code_signature || othersFormData?.electronic_signature;
|
othersFormData?.code_signature || othersFormData?.electronic_signature ||
|
||||||
|
continuationFormData?.puerto_entrada;
|
||||||
|
|
||||||
if (hasComplianceValue) {
|
if (hasComplianceValue) {
|
||||||
payload.compliance_mx = buildComplianceMxData(InvoiceTopFieldsFormData, generalFormData, othersFormData, observationFormData);
|
payload.compliance_mx = buildComplianceMxData(InvoiceTopFieldsFormData, generalFormData, othersFormData, observationFormData, continuationFormData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Financials
|
// Financials
|
||||||
@@ -169,7 +170,7 @@ function buildInvoicePayload(formData: FormDataSet): CreateInvoiceData | UpdateI
|
|||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildComplianceMxData(InvoiceTopFieldsFormData: any, generalFormData: any, othersFormData: any, observationFormData: any) {
|
function buildComplianceMxData(InvoiceTopFieldsFormData: any, generalFormData: any, othersFormData: any, observationFormData: any, continuationFormData: any) {
|
||||||
return {
|
return {
|
||||||
// Pedimento fields - desde InvoiceTopFieldsFormData
|
// Pedimento fields - desde InvoiceTopFieldsFormData
|
||||||
pedimento_id: InvoiceTopFieldsFormData?.pedimento_id ? Number(InvoiceTopFieldsFormData.pedimento_id) : null,
|
pedimento_id: InvoiceTopFieldsFormData?.pedimento_id ? Number(InvoiceTopFieldsFormData.pedimento_id) : null,
|
||||||
@@ -177,6 +178,7 @@ function buildComplianceMxData(InvoiceTopFieldsFormData: any, generalFormData: a
|
|||||||
is_pedimento_pending: Boolean(InvoiceTopFieldsFormData?.is_pedimento_pending || false),
|
is_pedimento_pending: Boolean(InvoiceTopFieldsFormData?.is_pedimento_pending || false),
|
||||||
// Fields from generalFormData
|
// Fields from generalFormData
|
||||||
aduana: generalFormData?.aduana || null,
|
aduana: generalFormData?.aduana || null,
|
||||||
|
port_of_entry: continuationFormData?.puerto_entrada || null,
|
||||||
provider_header: generalFormData?.provider_header || '',
|
provider_header: generalFormData?.provider_header || '',
|
||||||
provider_id: generalFormData?.provider_id || null,
|
provider_id: generalFormData?.provider_id || null,
|
||||||
sold_to_header: generalFormData?.sold_to_header || '',
|
sold_to_header: generalFormData?.sold_to_header || '',
|
||||||
|
|||||||
Reference in New Issue
Block a user