diff --git a/backend/api/v1/modules/a76/invoices/imports/temporary/validators/common.py b/backend/api/v1/modules/a76/invoices/imports/temporary/validators/common.py index 0440bf81..afba527a 100644 --- a/backend/api/v1/modules/a76/invoices/imports/temporary/validators/common.py +++ b/backend/api/v1/modules/a76/invoices/imports/temporary/validators/common.py @@ -12,6 +12,76 @@ from api.v1.modules.public.reference_data.currency_types.models import CurrencyT from api.v1.modules.public.reference_data.customs_sections.models import CustomsSection from ....models import TransportType, Currency, WeightUnit from core.exceptions import ErrorCollector +from typing import Dict, Any + + +def validate_required_fields_by_operation( + invoice_data: Dict[str, Any], + operation_type: str, + errors: ErrorCollector +) -> None: + """ + Valida campos obligatorios según tipo de operación. + Usar ANTES de guardar en BD. + """ + + # PROVEEDOR (SIEMPRE OBLIGATORIO - mensaje dinámico) + if not invoice_data.get('provider_id'): + # Mensaje dinámico según el header seleccionado + provider_labels = { + 'proveedor': 'Proveedor', + 'exportador': 'Exportador' + } + provider_header = invoice_data.get('provider_header') or 'proveedor' + field_label = provider_labels.get(provider_header, 'Proveedor') + + errors.add_error( + field="provider_id", + message=f"Debe seleccionar {field_label}", + solution=["Seleccione un proveedor de la lista desplegable"], + code="REQUIRED", + value=None + ) + + # VENDIDO A / CONSIGNADO A (SIEMPRE OBLIGATORIO - mensaje dinámico) + if not invoice_data.get('sold_to_id'): + # Mensaje dinámico según el header seleccionado + sold_to_labels = { + 'consignado_a': 'Consignado a', + 'vendido_a': 'Vendido a', + 'exportado_a': 'Exportado a', + 'importador': 'Importador' + } + sold_to_header = invoice_data.get('sold_to_header') or 'consignado_a' + field_label = sold_to_labels.get(sold_to_header, 'Cliente') + + errors.add_error( + field="sold_to_id", + message=f"Debe seleccionar {field_label}", + solution=["Seleccione una opción de la lista desplegable"], + code="REQUIRED", + value=None + ) + + # ENVIADO A (SIEMPRE OBLIGATORIO - mensaje fijo) + if not invoice_data.get('shipped_to_id'): + errors.add_error( + field="shipped_to_id", + message="Debe seleccionar el Destinatario", + solution=["Seleccione un destinatario de la lista desplegable"], + code="REQUIRED", + value=None + ) + + # AGENTE ADUANAL (OBLIGATORIO si hay pedimento) + if invoice_data.get('pedimento_id') and not invoice_data.get('customs_broker_id'): + errors.add_error( + field="customs_broker_id", + message="Debe seleccionar un Agente Aduanal", + solution=["Seleccione un agente aduanal de la lista desplegable"], + code="REQUIRED", + value=None + ) def validate_common( @@ -245,78 +315,87 @@ def validate_common( value=invoice.document_type, ) - provider_exists = ( - db.query(ClientProvider) - .filter( - ClientProvider.id == invoice.compliance_mx.provider_id, - ClientProvider.tenant_id == tenant_id, - ClientProvider.company_id == company_id, - ) - .first() - ) - if not provider_exists: - errors.add_error( - 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.compliance_mx.provider_id, + # Validar proveedor solo si se proporciona + if invoice.compliance_mx.provider_id: + provider_exists = ( + db.query(ClientProvider) + .filter( + ClientProvider.id == invoice.compliance_mx.provider_id, + ClientProvider.tenant_id == tenant_id, + ClientProvider.company_id == company_id, + ) + .first() ) + if not provider_exists: + errors.add_error( + 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.compliance_mx.provider_id, + ) - selled_to_exists = ( - db.query(ClientProvider) - .filter( - ClientProvider.id == invoice.compliance_mx.sold_to_id, - ClientProvider.tenant_id == tenant_id, - ClientProvider.company_id == company_id, - ) - .first() - ) - if not selled_to_exists: - errors.add_error( - 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.compliance_mx.sold_to_id, + # Validar vendido a solo si se proporciona + if invoice.compliance_mx.sold_to_id: + selled_to_exists = ( + db.query(ClientProvider) + .filter( + ClientProvider.id == invoice.compliance_mx.sold_to_id, + ClientProvider.tenant_id == tenant_id, + ClientProvider.company_id == company_id, + ) + .first() ) + if not selled_to_exists: + errors.add_error( + 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.compliance_mx.sold_to_id, + ) - shipped_to_exists = ( - db.query(ClientProvider) - .filter( - ClientProvider.id == invoice.compliance_mx.shipped_to_id, - ClientProvider.tenant_id == tenant_id, - ClientProvider.company_id == company_id, - ) - .first() - ) - if not shipped_to_exists: - errors.add_error( - 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.compliance_mx.shipped_to_id, + # Validar destinatario solo si se proporciona + if invoice.compliance_mx.shipped_to_id: + shipped_to_exists = ( + db.query(ClientProvider) + .filter( + ClientProvider.id == invoice.compliance_mx.shipped_to_id, + ClientProvider.tenant_id == tenant_id, + ClientProvider.company_id == company_id, + ) + .first() ) + if not shipped_to_exists: + errors.add_error( + 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.compliance_mx.shipped_to_id, + ) - customs_broker_exists = ( - db.query(CustomsBroker) - .filter( - CustomsBroker.id == invoice.compliance_mx.customs_broker_id, - CustomsBroker.tenant_id == tenant_id, - CustomsBroker.company_id == company_id, - ) - .first() - ) - if not customs_broker_exists: - errors.add_error( - 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.compliance_mx.customs_broker_id, + # Validar agente aduanal solo si se proporciona + if invoice.compliance_mx.customs_broker_id: + customs_broker_exists = ( + db.query(CustomsBroker) + .filter( + CustomsBroker.id == invoice.compliance_mx.customs_broker_id, + CustomsBroker.tenant_id == tenant_id, + CustomsBroker.company_id == company_id, + ) + .first() ) + if not customs_broker_exists: + errors.add_error( + 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.compliance_mx.customs_broker_id, + ) + # Validar transportista solo si se proporciona if invoice.logistics.carrier_id: carrier_exists = ( db.query(ClientProvider) diff --git a/backend/api/v1/modules/a76/invoices/imports/temporary/validators/create.py b/backend/api/v1/modules/a76/invoices/imports/temporary/validators/create.py index be6f6fc1..56800ff5 100644 --- a/backend/api/v1/modules/a76/invoices/imports/temporary/validators/create.py +++ b/backend/api/v1/modules/a76/invoices/imports/temporary/validators/create.py @@ -3,7 +3,7 @@ from sqlalchemy.orm import Session from api.v1.modules.a76.general_catalogs.exchange_rate.models import ExchangeRate from core.exceptions import ErrorCollector from ....schemas import InvoiceHeaderCreate -from .common import validate_common +from .common import validate_common, validate_required_fields_by_operation def validate_create(db: Session, invoice: InvoiceHeaderCreate, tenant_id: int, company_id: int, errors: ErrorCollector) -> None: """ Valida la creación de una nueva factura de importe temporal """ @@ -21,22 +21,31 @@ def validate_create(db: Session, invoice: InvoiceHeaderCreate, tenant_id: int, c errors.add_required_error("invoice_number") if not invoice.invoice_date: - errors.add_required_error("invoice_date") - - if not invoice.compliance_mx.provider_id: - errors.add_required_error("compliance_mx.provider_id") + errors.add_required_error("invoice_date") - if not invoice.compliance_mx.sold_to_id: - errors.add_required_error("compliance_mx.sold_to_id") - - if not invoice.compliance_mx.shipped_to_id: - errors.add_required_error("compliance_mx.shipped_to_id") - - if not invoice.compliance_mx.customs_broker_id: - errors.add_required_error("compliance_mx.customs_broker_id") - if errors.has_errors(): - """Se retorna por que hay campos obligatiorios para las validaciones que tienen que ser llenados""" + """Se retorna porque hay campos obligatorios básicos que deben ser llenados""" + return + + # Validar campos obligatorios según tipo de operación + invoice_data = { + 'provider_header': invoice.compliance_mx.provider_header if invoice.compliance_mx else None, + 'provider_id': invoice.compliance_mx.provider_id if invoice.compliance_mx else None, + 'sold_to_id': invoice.compliance_mx.sold_to_id if invoice.compliance_mx else None, + 'sold_to_header': invoice.compliance_mx.sold_to_header if invoice.compliance_mx else None, + 'shipped_to_id': invoice.compliance_mx.shipped_to_id if invoice.compliance_mx else None, + 'customs_broker_id': invoice.compliance_mx.customs_broker_id if invoice.compliance_mx else None, + 'pedimento_id': invoice.compliance_mx.pedimento_id if invoice.compliance_mx else None, + } + + validate_required_fields_by_operation( + invoice_data=invoice_data, + operation_type=invoice.operation_type, + errors=errors + ) + + if errors.has_errors(): + """Se retorna porque hay campos obligatorios según el tipo de operación que deben ser llenados""" return validate_common(db, invoice, tenant_id, company_id, errors) diff --git a/backend/api/v1/modules/a76/invoices/imports/temporary/validators/update.py b/backend/api/v1/modules/a76/invoices/imports/temporary/validators/update.py index 58efd41a..da4cc970 100644 --- a/backend/api/v1/modules/a76/invoices/imports/temporary/validators/update.py +++ b/backend/api/v1/modules/a76/invoices/imports/temporary/validators/update.py @@ -5,6 +5,7 @@ from decimal import Decimal from core.exceptions import ErrorCollector from ....schemas import InvoiceHeaderUpdate from ....models import InvoiceHeader +from .common import validate_required_fields_by_operation # Helper function para limpiar strings (equivalente a Clip()) @@ -33,6 +34,22 @@ def validate_update( None (modifica invoice_data in-place y acumula errores en errors) """ + # Validar campos requeridos según el tipo de operación + invoice_dict = { + 'provider_id': invoice_data.compliance_mx.provider_id if invoice_data.compliance_mx else None, + 'sold_to_id': invoice_data.compliance_mx.sold_to_id if invoice_data.compliance_mx else None, + 'sold_to_header': invoice_data.compliance_mx.sold_to_header if invoice_data.compliance_mx else None, + 'shipped_to_id': invoice_data.compliance_mx.shipped_to_id if invoice_data.compliance_mx else None, + 'customs_broker_id': invoice_data.compliance_mx.customs_broker_id if invoice_data.compliance_mx else None, + 'pedimento_id': invoice_data.compliance_mx.pedimento_id if invoice_data.compliance_mx else None, + } + + validate_required_fields_by_operation( + invoice_data=invoice_dict, + operation_type=invoice_data.operation_type or 'IMP', + errors=errors + ) + # Primero ejecutar validaciones comunes # validate_common(invoice_data, errors) diff --git a/backend/api/v1/modules/a76/invoices/schemas.py b/backend/api/v1/modules/a76/invoices/schemas.py index af111dc8..a1324e69 100644 --- a/backend/api/v1/modules/a76/invoices/schemas.py +++ b/backend/api/v1/modules/a76/invoices/schemas.py @@ -113,17 +113,17 @@ class InvoiceComplianceMxBase(BaseModel): manifest_number: Optional[str] = Field( None, max_length=15, description="Manifest number" ) - provider_header: str = Field(None, max_length=20, description="Provider header") - provider_id: int = Field(None, description="Provider ID") - sold_to_header: str = Field(None, max_length=20, description="Sold to header") - sold_to_id: int = Field(None, description="Sold to ID") - shipped_to_header: str = Field(None, max_length=20, description="Shipped to header") - shipped_to_id: int = Field(None, description="Shipped to ID") + provider_header: Optional[str] = Field(None, max_length=20, description="Provider header") + provider_id: Optional[int] = Field(None, description="Provider ID") + sold_to_header: Optional[str] = Field(None, max_length=20, description="Sold to header") + sold_to_id: Optional[int] = Field(None, description="Sold to ID") + shipped_to_header: Optional[str] = Field(None, max_length=20, description="Shipped to header") + shipped_to_id: Optional[int] = Field(None, description="Shipped to ID") shipped_by_header: Optional[int] = Field( None, max_length=20, description="Shipped by header" ) shipped_by_id: Optional[int] = Field(None, description="Shipped by ID") - customs_broker_id: int = Field(None, description="Customs broker ID") + customs_broker_id: Optional[int] = Field(None, description="Customs broker ID") customs_broker_us_id: Optional[int] = Field( None, description="US customs broker ID" ) diff --git a/frontend/src/lib/components/dashboard/invoices/edit/general-tab-form.svelte b/frontend/src/lib/components/dashboard/invoices/edit/general-tab-form.svelte index b312246a..f9c18642 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/general-tab-form.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/general-tab-form.svelte @@ -7,11 +7,12 @@ import type { InvoiceType } from '$lib/api/dashboard/refrence_data/invoice_types'; import type { CustomsBroker } from '$lib/api/dashboard/a76/customs-brokers'; import type { ClientProvider } from '$lib/api/dashboard/a76/clients-providers'; - import { - "sidebar.client_provider_type.client_indicator" as clientIndicator, - "sidebar.client_provider_type.provider_indicator" as providerIndicator, - "sidebar.client_provider_type.both_indicator" as bothIndicator - } from '$lib/paraglide/messages.js'; + + interface CodePedimentoRegimen { + regimen_code: string; + type_code: string; + [key: string]: any; + } let { invoice, @@ -27,6 +28,7 @@ customsSections = [], codePedimentoRegimens = [], operationType = undefined, + defaultOperationType = undefined, exchangeRate = undefined }: { invoice: Invoice | null; @@ -42,8 +44,8 @@ drivers?: any[]; trailers?: any[]; customsSections?: any[]; - codePedimentoRegimens?: any[]; - defaultOperationType?: string | null; + codePedimentoRegimens?: CodePedimentoRegimen[]; + defaultOperationType?: string | number | null; defaultInvoiceType?: string | null; operationType?: number | null; exchangeRate?: number | null; @@ -200,21 +202,35 @@ return Array.from(uniqueMap.values()); }); + // Determinar tipo de código basado en operationType prop, defaultOperationType o invoice.operation_type + const typeCode = $derived( + operationType === 1 ? 'E' + : operationType === 2 ? 'I' + : defaultOperationType === 1 ? 'E' + : defaultOperationType === 2 ? 'I' + : defaultOperationType === 'exp' ? 'E' + : defaultOperationType === 'imp' ? 'I' + : invoice?.operation_type === 'exp' ? 'E' + : invoice?.operation_type === 'imp' ? 'I' + : null + ); + // Filtrar regímenes por tipo de operación (1='E' exp, 2='I' imp) y obtener valores únicos - const filteredRegimens = $derived.by(() => { - const typeCode = operationType === 1 ? 'E' : operationType === 2 ? 'I' : null; - const filtered = codePedimentoRegimens.filter(r => r.type_code === typeCode); - - // Obtener solo regímenes únicos por regimen_code - const uniqueMap = new Map(); - filtered.forEach(r => { - if (r.regimen_code && !uniqueMap.has(r.regimen_code)) { - uniqueMap.set(r.regimen_code, r); - } - }); - - return Array.from(uniqueMap.values()); - }); + const filteredRegimens = $derived( + !codePedimentoRegimens || codePedimentoRegimens.length === 0 || !typeCode + ? [] + : Array.from( + codePedimentoRegimens + .filter(r => r.type_code === typeCode) + .reduce((map, r) => { + if (r.regimen_code && !map.has(r.regimen_code)) { + map.set(r.regimen_code, r); + } + return map; + }, new Map()) + .values() + ) + ); // Efecto: Limpiar régimen si no existe en los regímenes filtrados al cambiar operation_type $effect(() => { @@ -284,9 +300,11 @@ > - {formData.provider_id - ? providers.find(p => p.id === formData.provider_id)?.name || 'Selecciona...' - : 'Selecciona...'} + {#if formData.provider_id} + {providers.find(p => p.id === formData.provider_id)?.name || 'Selecciona...'} + {:else} + Selecciona... + {/if} @@ -330,9 +348,11 @@ > - {formData.sold_to_id - ? clients.find(c => c.id === formData.sold_to_id)?.name || 'Selecciona...' - : 'Selecciona...'} + {#if formData.sold_to_id} + {clients.find(c => c.id === formData.sold_to_id)?.name || 'Selecciona...'} + {:else} + Selecciona... + {/if} @@ -376,15 +396,17 @@ > - {formData.shipped_to_id - ? allClientsProviders.find(cp => cp.id === formData.shipped_to_id)?.name || 'Selecciona...' - : 'Selecciona...'} + {#if formData.shipped_to_id} + {allClientsProviders.find(cp => cp.id === formData.shipped_to_id)?.name || 'Selecciona...'} + {:else} + Selecciona... + {/if} {#each allClientsProviders as cp} - {cp.name} ({cp.type === 'client' ? clientIndicator() : cp.type === 'provider' ? providerIndicator() : bothIndicator()}) + {cp.name} {/each} diff --git a/frontend/src/lib/components/dashboard/invoices/edit/invoice-top-fields.svelte b/frontend/src/lib/components/dashboard/invoices/edit/invoice-top-fields.svelte index 47743eef..e3549f05 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/invoice-top-fields.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/invoice-top-fields.svelte @@ -46,6 +46,16 @@ } }); + // Efecto para actualizar operation_type cuando cambia defaultOperationType + $effect(() => { + if (formData && defaultOperationType !== undefined && defaultOperationType !== null) { + // Si operation_type está vacío, null, o undefined, actualizarlo con defaultOperationType + if (!formData.operation_type) { + formData.operation_type = defaultOperationType; + } + } + }); + if (!formData) { let operationType: string | null = null; if (invoice?.operation_type) { @@ -69,6 +79,11 @@ clave_pedimento: '', regimen_pedimento: '', }; + } else { + // Si formData ya existe pero operation_type está vacío, usar defaultOperationType + if (!formData.operation_type && defaultOperationType !== undefined && defaultOperationType !== null) { + formData.operation_type = defaultOperationType; + } } diff --git a/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts b/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts index fbd26b64..03982769 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts +++ b/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts @@ -97,7 +97,7 @@ export async function saveInvoice(options: SaveInvoiceOptions): Promise import { onMount } from 'svelte'; import { browser } from '$app/environment'; - import { goto } from '$app/navigation'; + import { goto } from '$app/navigation'; + import { page } from '$app/stores'; import * as Tabs from '$lib/components/ui/tabs'; import { Button } from '$lib/components/ui/button'; import { Badge } from '$lib/components/ui/badge'; @@ -169,7 +170,7 @@ if (e && typeof e === 'object' && 'validationErrors' in e && Array.isArray((e as any).validationErrors)) { const validationErrors = (e as any).validationErrors; const errorList = validationErrors.map((err: any) => - `• ${err.field}: ${err.message}${err.solution ? ' - ' + err.solution.join(', ') : ''}` + `• ${err.message}` ).join('\n'); toast.error(errorMessage, { @@ -259,7 +260,13 @@ codePedimentoRegimens={data.codePedimentoRegimens || []} defaultOperationType={data.filters?.operation_type ?? undefined} defaultInvoiceType={data.filters?.invoice_type ?? undefined} - operationType={InvoiceTopFieldsFormData?.operation_type} + operationType={ + InvoiceTopFieldsFormData?.operation_type === 'exp' ? 1 + : InvoiceTopFieldsFormData?.operation_type === 'imp' ? 2 + : data.invoice?.operation_type === 'exp' ? 1 + : data.invoice?.operation_type === 'imp' ? 2 + : undefined + } exchangeRate={calculatedExchangeRate} />