feat: enhance invoice validation and error handling with detailed messages
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -10,6 +10,13 @@ const API_BASE_URL = (import.meta.env.VITE_API_URL || '').replace(/\/+$/, '');
|
||||
export interface ApiResponse<T = any> {
|
||||
data?: T;
|
||||
error?: string;
|
||||
validationErrors?: Array<{
|
||||
field: string;
|
||||
message: string;
|
||||
code?: string;
|
||||
solution?: string[];
|
||||
value?: any;
|
||||
}>;
|
||||
status: number;
|
||||
}
|
||||
|
||||
@@ -207,30 +214,41 @@ async function fetchApi<T = any>(
|
||||
|
||||
if (!response.ok) {
|
||||
// Manejo especial para errores 422 (validation error)
|
||||
if (response.status === 422 && data.detail) {
|
||||
let errorMessage = 'Error de validación: ';
|
||||
|
||||
// FastAPI devuelve errores de validación en data.detail como array
|
||||
if (Array.isArray(data.detail)) {
|
||||
const errors = data.detail.map((err: any) => {
|
||||
const field = err.loc ? err.loc.join('.') : 'campo desconocido';
|
||||
return `${field}: ${err.msg}`;
|
||||
}).join(', ');
|
||||
errorMessage += errors;
|
||||
} else if (typeof data.detail === 'string') {
|
||||
errorMessage = data.detail;
|
||||
} else {
|
||||
errorMessage += JSON.stringify(data.detail);
|
||||
if (response.status === 422) {
|
||||
// Errores de validación personalizados (con array errors)
|
||||
if (data.errors && Array.isArray(data.errors)) {
|
||||
return {
|
||||
error: data.message || 'Error de validación',
|
||||
validationErrors: data.errors,
|
||||
status: response.status
|
||||
};
|
||||
}
|
||||
// Errores de validación de FastAPI (con detail)
|
||||
else if (data.detail) {
|
||||
let errorMessage = 'Error de validación: ';
|
||||
|
||||
// FastAPI devuelve errores de validación en data.detail como array
|
||||
if (Array.isArray(data.detail)) {
|
||||
const errors = data.detail.map((err: any) => {
|
||||
const field = err.loc ? err.loc.join('.') : 'campo desconocido';
|
||||
return `${field}: ${err.msg}`;
|
||||
}).join(', ');
|
||||
errorMessage += errors;
|
||||
} else if (typeof data.detail === 'string') {
|
||||
errorMessage = data.detail;
|
||||
} else {
|
||||
errorMessage += JSON.stringify(data.detail);
|
||||
}
|
||||
|
||||
return {
|
||||
error: errorMessage,
|
||||
status: response.status
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
error: errorMessage,
|
||||
status: response.status
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
error: data.detail || 'Error en la petición',
|
||||
error: data.message || data.detail || 'Error en la petición',
|
||||
status: response.status
|
||||
};
|
||||
}
|
||||
|
||||
@@ -276,7 +276,7 @@ export interface CreateInvoiceData {
|
||||
enajenation_goods?: boolean | null;
|
||||
compliance_mx?: Omit<InvoiceComplianceMx, 'invoice_id'> | null;
|
||||
financials?: Omit<InvoiceFinancials, 'id' | 'invoice_id'> | null;
|
||||
logistics?: Omit<InvoiceLogistics, 'id' | 'invoice_id'>[] | null;
|
||||
logistics?: Omit<InvoiceLogistics, 'id' | 'invoice_id'> | null;
|
||||
details?: Omit<InvoiceSalesDetails, 'id' | 'invoice_id'>[] | null;
|
||||
collections?: Omit<InvoiceCollections, 'id' | 'invoice_id'>[] | null;
|
||||
}
|
||||
|
||||
@@ -11,20 +11,16 @@
|
||||
let {
|
||||
invoice,
|
||||
formData = $bindable(),
|
||||
invoiceTypes = [],
|
||||
customsBrokers = [],
|
||||
clients = [],
|
||||
providers = [],
|
||||
currencyTypes = [],
|
||||
transportTypes = [],
|
||||
transporters = [],
|
||||
vehicles = [],
|
||||
drivers = [],
|
||||
trailers = [],
|
||||
customsSections = [],
|
||||
codePedimentoRegimens = [],
|
||||
defaultOperationType = undefined,
|
||||
defaultInvoiceType = undefined,
|
||||
operationType = undefined,
|
||||
exchangeRate = undefined
|
||||
}: {
|
||||
@@ -76,7 +72,7 @@
|
||||
weight_type: 'kgs',
|
||||
iva_factor: invoice.financials?.iva_factor || null,
|
||||
carrier_id: invoice.logistics?.[0]?.carrier_id || null,
|
||||
transport_id: '',
|
||||
transport_id: invoice.logistics?.[0]?.transport_id || '',
|
||||
driver_name: invoice.logistics?.[0]?.driver_name || '',
|
||||
transport_type: invoice.logistics?.[0]?.transport_type || '',
|
||||
transport_num: invoice.logistics?.[0]?.vehicle_num || '',
|
||||
@@ -133,6 +129,21 @@
|
||||
{ value: 'lbs', label: 'Libras (lb)' }
|
||||
];
|
||||
|
||||
// Opciones de tipo de transporte
|
||||
const transportTypeOptions = [
|
||||
{ value: 'none', label: 'Ninguno' },
|
||||
{ value: 'transport', label: 'Transporte' },
|
||||
{ value: 'box', label: 'Caja' },
|
||||
{ value: 'licence_plates', label: 'Placas' },
|
||||
{ value: 'truck', label: 'Camión' },
|
||||
{ value: 'vessel', label: 'Buque' },
|
||||
{ value: 'rail_barge', label: 'Ferrobarcaza' },
|
||||
{ value: 'container', label: 'Contenedor' },
|
||||
{ value: 'airplane', label: 'Avión' },
|
||||
{ value: 'gondola', label: 'Góndola' },
|
||||
{ value: 'flatbed', label: 'Plataforma' }
|
||||
];
|
||||
|
||||
// Opciones de encabezados
|
||||
const providerHeaderOptions = [
|
||||
{ value: 'proveedor', label: 'Proveedor' },
|
||||
@@ -540,18 +551,18 @@
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 space-y-1.5">
|
||||
<Label for="transport_type" class="text-xs">Clave Transporte:</Label>
|
||||
<Label for="transport_id" class="text-xs">Clave Transporte:</Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.transport_type || ''}
|
||||
value={formData.transport_id || ''}
|
||||
onValueChange={(v) => {
|
||||
formData.transport_type = v ?? '';
|
||||
formData.transport_id = v ?? '';
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="transport_type" class="h-7 text-xs min-w-[120px] max-w-[250px]">
|
||||
<Select.Trigger id="transport_id" class="h-7 text-xs min-w-[120px] max-w-[250px]">
|
||||
<span class="truncate">
|
||||
{#if formData.transport_type}
|
||||
{vehicles.find(v => v.vehicle_key === formData.transport_type)?.vehicle_key || formData.transport_type}
|
||||
{#if formData.transport_id}
|
||||
{vehicles.find(v => v.vehicle_key === formData.transport_id)?.vehicle_key || formData.transport_id}
|
||||
{:else if vehicles.length > 0}
|
||||
Selecciona vehículo...
|
||||
{:else}
|
||||
@@ -605,28 +616,22 @@
|
||||
<Label for="transport_type" class="text-xs">Tipo Transporte:</Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.transport_id || 'Ninguno'}
|
||||
value={formData.transport_type || 'none'}
|
||||
onValueChange={(v) => {
|
||||
formData.transport_id = v ?? 'Ninguno';
|
||||
formData.transport_type = v ?? 'none';
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="transport_id" class="h-7 text-xs min-w-[120px] max-w-[250px]">
|
||||
<Select.Trigger id="transport_type" class="h-7 text-xs min-w-[120px] max-w-[250px]">
|
||||
<span class="truncate">
|
||||
{formData.transport_id || 'Ninguno'}
|
||||
{transportTypeOptions.find(t => t.value === formData.transport_type)?.label || 'Ninguno'}
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="Ninguno">Ninguno</Select.Item>
|
||||
<Select.Item value="Transporte">Transporte</Select.Item>
|
||||
<Select.Item value="Caja">Caja</Select.Item>
|
||||
<Select.Item value="Placas">Placas</Select.Item>
|
||||
<Select.Item value="Camión">Camión</Select.Item>
|
||||
<Select.Item value="Buque">Buque</Select.Item>
|
||||
<Select.Item value="Ferrobarcaza">Ferrobarcaza</Select.Item>
|
||||
<Select.Item value="Contenedor">Contenedor</Select.Item>
|
||||
<Select.Item value="Avion">Avion</Select.Item>
|
||||
<Select.Item value="Gondola">Gondola</Select.Item>
|
||||
<Select.Item value="Plataforma">Plataforma</Select.Item>
|
||||
{#each transportTypeOptions as option}
|
||||
<Select.Item value={option.value}>
|
||||
{option.label}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,20 @@ interface SaveInvoiceOptions {
|
||||
formData: FormDataSet;
|
||||
}
|
||||
|
||||
export async function saveInvoice(options: SaveInvoiceOptions): Promise<{ success: boolean; error?: string; newInvoiceId?: number }> {
|
||||
interface SaveInvoiceResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
newInvoiceId?: number;
|
||||
validationErrors?: Array<{
|
||||
field: string;
|
||||
message: string;
|
||||
code?: string;
|
||||
solution?: string[];
|
||||
value?: any;
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function saveInvoice(options: SaveInvoiceOptions): Promise<SaveInvoiceResult> {
|
||||
const { invoiceId, isCreate, companyId, formData } = options;
|
||||
const { InvoiceTopFieldsFormData, generalFormData, observationFormData, itemsFormData, othersFormData, continuationFormData } = formData;
|
||||
|
||||
@@ -61,9 +74,13 @@ export async function saveInvoice(options: SaveInvoiceOptions): Promise<{ succes
|
||||
if (isCreate) {
|
||||
// Crear nueva factura con todos sus sub-recursos
|
||||
const response = await invoicesApi.create(companyId, payload as CreateInvoiceData);
|
||||
console.log('Create response:', response);
|
||||
if (response.error) {
|
||||
const errorMsg = typeof response.error === 'string' ? response.error : 'Error al crear la factura';
|
||||
throw new Error(errorMsg);
|
||||
const error: any = new Error(errorMsg);
|
||||
error.validationErrors = response.validationErrors;
|
||||
console.log('Throwing error with validationErrors:', error.validationErrors);
|
||||
throw error;
|
||||
}
|
||||
if (!response.data?.id) throw new Error('No se recibió el ID de la factura creada');
|
||||
newInvoiceId = response.data.id;
|
||||
@@ -73,13 +90,22 @@ export async function saveInvoice(options: SaveInvoiceOptions): Promise<{ succes
|
||||
} else {
|
||||
// Actualizar factura existente con todos sus sub-recursos
|
||||
const response = await invoicesApi.update(invoiceId!, companyId, payload as UpdateInvoiceData);
|
||||
if (response.error) throw new Error(response.error);
|
||||
console.log('Update response:', response);
|
||||
if (response.error) {
|
||||
const error: any = new Error(response.error);
|
||||
error.validationErrors = response.validationErrors;
|
||||
console.log('Throwing error with validationErrors:', error.validationErrors);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, newInvoiceId: newInvoiceId ?? undefined };
|
||||
} catch (e) {
|
||||
console.log('Caught error in saveInvoice:', e);
|
||||
console.log('Error has validationErrors?', (e as any)?.validationErrors);
|
||||
const error = e instanceof Error ? e.message : 'Error al guardar los cambios';
|
||||
return { success: false, error };
|
||||
const validationErrors = (e as any)?.validationErrors;
|
||||
return { success: false, error, validationErrors };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,14 +162,7 @@ function buildInvoicePayload(formData: FormDataSet): CreateInvoiceData | UpdateI
|
||||
}
|
||||
|
||||
// Logistics
|
||||
const hasLogisticsFromGeneral = generalFormData?.carrier_id ||
|
||||
generalFormData?.driver_name || generalFormData?.transport_type || generalFormData?.transport_num;
|
||||
|
||||
const hasLogisticsFromObservations = observationFormData?.incoterm;
|
||||
|
||||
if (hasLogisticsFromGeneral || hasLogisticsFromObservations) {
|
||||
payload.logistics = buildLogisticsData(generalFormData, observationFormData);
|
||||
}
|
||||
payload.logistics = buildLogisticsData(generalFormData, observationFormData);
|
||||
|
||||
// Eliminar campos undefined para no enviarlos
|
||||
Object.keys(payload).forEach(key => {
|
||||
@@ -206,12 +225,12 @@ function buildFinancialsData(generalFormData: any, observationFormData: any, oth
|
||||
}
|
||||
|
||||
function buildLogisticsData(generalFormData: any, observationFormData: any) {
|
||||
// Crear un solo entry de logistics con los datos de generalFormData y observationFormData
|
||||
return [{
|
||||
// Retornar logistics como objeto único
|
||||
return {
|
||||
carrier_id: generalFormData?.carrier_id || null,
|
||||
transport_type: generalFormData?.transport_type || null,
|
||||
transport_type: generalFormData?.transport_type || 'none',
|
||||
driver_name: generalFormData?.driver_name || null,
|
||||
vehicle_num: generalFormData?.transport_num || null,
|
||||
incoterm: observationFormData?.incoterm || null,
|
||||
}];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -144,13 +144,23 @@
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Save result:', result);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Error al guardar la factura');
|
||||
// Crear un error con validationErrors si existen
|
||||
const error: any = new Error(result.error || 'Error al guardar la factura');
|
||||
if (result.validationErrors) {
|
||||
error.validationErrors = result.validationErrors;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
toast.success('Todos los cambios se guardaron correctamente');
|
||||
} catch (e) {
|
||||
console.error('Error saving all:', e);
|
||||
console.log('Error object:', e);
|
||||
console.log('Has validationErrors?', (e as any)?.validationErrors);
|
||||
|
||||
if (e instanceof Error && e.message.includes('401')) {
|
||||
toast.error('Sesión expirada. Recargando página...');
|
||||
setTimeout(() => {
|
||||
@@ -158,9 +168,23 @@
|
||||
}, 1500);
|
||||
} else {
|
||||
const errorMessage = e instanceof Error ? e.message : 'Error al guardar los cambios';
|
||||
toast.error(errorMessage, {
|
||||
description: 'Revisa la consola para más detalles'
|
||||
});
|
||||
|
||||
// Si hay errores de validación, mostrarlos en detalle
|
||||
if (e && typeof e === 'object' && 'validationErrors' in e && Array.isArray((e as any).validationErrors)) {
|
||||
const validationErrors = (e as any).validationErrors;
|
||||
console.log('Found validationErrors:', validationErrors);
|
||||
const errorList = validationErrors.map((err: any) =>
|
||||
`• ${err.field}: ${err.message}${err.solution ? ' - ' + err.solution.join(', ') : ''}`
|
||||
).join('\n');
|
||||
|
||||
toast.error(errorMessage, {
|
||||
description: errorList
|
||||
});
|
||||
} else {
|
||||
toast.error(errorMessage, {
|
||||
description: 'Revisa la consola para más detalles'
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
saving = false;
|
||||
|
||||
Reference in New Issue
Block a user