feat: Enhance invoice validation and calculations

- Added new validations for pedimento regimes and compliance checks in common_validators.py.
- Implemented missing date checks for consolidated pedimentos.
- Introduced validation for shipped_by_id and manifest_number in invoice compliance.
- Refactored create_validators.py by removing the file as it was no longer needed.
- Updated update validators for exports and imports to use invoice_data instead of invoice.
- Modified schemas.py to set default values for financial fields to avoid None values.
- Created calculations.py to handle invoice calculations, including total increments and regime changes.
- Improved layout responsiveness in various Svelte components for better user experience.
This commit is contained in:
2026-03-10 11:29:38 -05:00
parent a578135183
commit c2d691a4ad
13 changed files with 268 additions and 124 deletions

View File

@@ -98,7 +98,7 @@
</script>
<!-- Layout de 2 columnas compacto -->
<div class="grid grid-cols-2 gap-3">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<!-- Columna Izquierda -->
<div class="space-y-3 rounded-md border p-3">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Información General</h4>
@@ -148,7 +148,7 @@
</div>
<!-- DESTINO/ORIGEN Y ES MIXTO -->
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1.5">
<Label for="destino_origen" class="text-xs">Destino/Origen:</Label>
<Input
@@ -165,7 +165,7 @@
<RadioGroup
value={String(formData.is_mixed)}
onValueChange={(v: string | undefined) => (formData.is_mixed = v === 'true')}
class="flex gap-4"
class="flex flex-wrap gap-4"
>
<div class="flex items-center gap-2">
<RadioGroupItem value="true" id="mixed_si" />
@@ -205,7 +205,7 @@
<div class="space-y-3 pt-1">
<div class="space-y-1.5">
<Label class="text-xs">Razón de exportación:</Label>
<RadioGroup bind:value={formData.reason_export} class="flex gap-4">
<RadioGroup bind:value={formData.reason_export} class="flex flex-wrap gap-4">
<div class="flex items-center gap-2">
<RadioGroupItem value="1" id="reason_vendido" />
<Label for="reason_vendido" class="text-xs text-muted-foreground">Vendido</Label>
@@ -245,7 +245,7 @@
{/if}
<!-- CHECKBOXES -->
<div class="grid grid-cols-2 gap-2">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
{#if operationType !== 1 && invoiceType !== 'CR'}
<div class="flex items-center gap-2">
<Checkbox id="fue_revisado" bind:checked={formData.fue_revisado_equipo} />

View File

@@ -302,12 +302,12 @@
</script>
<!-- Layout de 2 columnas compacto -->
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Columna Izquierda: Clientes - Proveedores - Agente Aduanal -->
<div class="space-y-3 rounded-md border p-3">
{#if invoiceType !== 'MEX'}
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Datos del pedimento</h4>
<div class="grid grid-cols-4 gap-3 text-xs">
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 text-xs">
<div>
<span class="text-muted-foreground">Fecha del:</span>
<p class="font-medium">{formData.fecha_pedimento_del || '-'}</p>
@@ -330,7 +330,8 @@
<h4 class="text-xs font-semibold text-muted-foreground uppercase">
Clientes - Proveedores - Agente Aduanal
</h4>
<div class="grid grid-cols-4 gap-3 space-y-1.5">
<div class="grid grid-cols-[1fr_4fr_4fr] items-center gap-x-2 gap-y-2 min-w-0">
<!-- Row 1: Proveedor / Exportador -->
<Select.Root
type="single"
value={formData.provider_header || providerHeaderOptions[0]?.value || ''}
@@ -338,7 +339,7 @@
formData.provider_header = v ?? '';
}}
>
<Select.Trigger id="provider_header" class="h-7 max-w-[250px] min-w-[125px] text-xs">
<Select.Trigger id="provider_header" class="h-7 w-full text-xs">
<span class="truncate">
{providerHeaderOptions.find(
(o) => o.value === (formData.provider_header || providerHeaderOptions[0]?.value)
@@ -360,7 +361,7 @@
formData.provider_id = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="provider_id" class="h-7 max-w-[250px] min-w-[120px] text-xs">
<Select.Trigger id="provider_id" class="h-7 w-full text-xs">
<span class="truncate">
{#if formData.provider_id}
{providers.find((p) => p.id === formData.provider_id)?.name || 'Selecciona...'}
@@ -377,10 +378,9 @@
{/each}
</Select.Content>
</Select.Root>
<span class="text-red-500">*</span>
</div>
<span class="text-red-500 font-bold">*</span>
<div class="grid grid-cols-4 gap-3 space-y-1.5">
<!-- Row 2: Consignado a / Vendido a / Importador -->
<Select.Root
type="single"
value={formData.sold_to_header || soldToHeaderOptions[0]?.value || ''}
@@ -388,7 +388,7 @@
formData.sold_to_header = v ?? '';
}}
>
<Select.Trigger id="sold_to_header" class="h-7 max-w-[250px] min-w-[125px] text-xs">
<Select.Trigger id="sold_to_header" class="h-7 w-full text-xs">
<span class="truncate">
{soldToHeaderOptions.find(
(o) => o.value === (formData.sold_to_header || soldToHeaderOptions[0]?.value)
@@ -410,7 +410,7 @@
formData.sold_to_id = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="sold_to_id" class="h-7 max-w-[250px] min-w-[120px] text-xs">
<Select.Trigger id="sold_to_id" class="h-7 w-full text-xs">
<span class="truncate">
{#if formData.sold_to_id}
{clients.find((c) => c.id === formData.sold_to_id)?.name || 'Selecciona...'}
@@ -427,10 +427,9 @@
{/each}
</Select.Content>
</Select.Root>
<span class="text-red-500">*</span>
</div>
<span class="text-red-500 font-bold">*</span>
<div class="grid grid-cols-4 gap-3 space-y-1.5">
<!-- Row 3: Enviado a -->
<Select.Root
type="single"
value={formData.shipped_to_header || shippedToHeaderOptions[0]?.value || ''}
@@ -438,7 +437,7 @@
formData.shipped_to_header = v ?? '';
}}
>
<Select.Trigger id="shipped_to_header" class="h-7 text-xs min-w-[125px] max-w-[250px]">
<Select.Trigger id="shipped_to_header" class="h-7 w-full text-xs">
<span class="truncate">
{shippedToHeaderOptions.find(
(o) => o.value === (formData.shipped_to_header || shippedToHeaderOptions[0]?.value)
@@ -460,7 +459,7 @@
formData.shipped_to_id = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="shipped_to_id" class="h-7 text-xs min-w-[120px] max-w-[250px]">
<Select.Trigger id="shipped_to_id" class="h-7 w-full text-xs">
<span class="truncate">
{#if formData.shipped_to_id}
{allClientsProviders.find((cp) => cp.id === formData.shipped_to_id)?.name ||
@@ -478,7 +477,7 @@
{/each}
</Select.Content>
</Select.Root>
<span class="text-red-500">*</span>
<span class="text-red-500 font-bold">*</span>
</div>
<div class="space-y-1.5">
@@ -542,7 +541,7 @@
<div class="space-y-3">
<!-- Tipo de Moneda - Pesos Netos y Brutos -->
<div class="border rounded-md p-3 space-y-2">
<div class="flex justify-between">
<div class="flex flex-wrap justify-between gap-1">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">
Tipo de Moneda - Pesos Netos y Brutos
</h4>
@@ -562,7 +561,7 @@
<!-- Radio buttons para tipo de moneda -->
<div class="space-y-1.5">
<RadioGroup.Root bind:value={formData.currency} class="flex gap-4">
<RadioGroup.Root bind:value={formData.currency} class="flex flex-wrap gap-x-4 gap-y-2">
<div class="flex items-center space-x-2">
<RadioGroup.Item value="foreign" id="currency-foreign" class="h-4 w-4" />
<Label for="currency-foreign" class="text-xs font-normal cursor-pointer"
@@ -608,7 +607,7 @@
</Select.Root>
</div>
{/if}
<div class="grid grid-cols-3 gap-2">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2">
<div class="space-y-1.5">
<Label for="weight_type" class="text-xs">Tipo Peso:</Label>
<Select.Root
@@ -675,7 +674,7 @@
<div class="border rounded-md p-3 space-y-2">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Transportista</h4>
<div class="grid grid-cols-3 gap-2">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2">
{#if invoiceType !== 'MEX'}
<div class="space-y-1.5">
<Label for="carrier_id" class="text-xs">Transportista:</Label>
@@ -773,7 +772,7 @@
</Select.Root>
</div>
<div class="grid grid-cols-4 gap-2">
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2">
<div class="space-y-1.5">
<Label for="transport_type" class="text-xs">Tipo Transporte:</Label>
<Select.Root
@@ -799,7 +798,7 @@
</Select.Root>
</div>
<div class="col-span-3 space-y-1.5">
<div class="col-span-1 sm:col-span-3 space-y-1.5">
<Label for="transport_num" class="text-xs">Placas:</Label>
<Select.Root
type="single"

View File

@@ -124,8 +124,8 @@
</script>
<!-- Datos Principales en una fila compacta (reusable across tabs) -->
<div class="grid grid-cols-12 items-end gap-3 pb-3">
<div class="col-span-1 space-y-1">
<div class="flex flex-wrap items-end gap-3 pb-3">
<div class="min-w-[100px] flex-1 space-y-1">
<Label for="operation_type" class="text-xs"
>Tipo de Operación<span class="text-red-500">*</span></Label
>
@@ -147,7 +147,7 @@
</Select.Content>
</Select.Root>
</div>
<div class="col-span-1 space-y-1">
<div class="min-w-[100px] flex-1 space-y-1">
<Label for="operation_type" class="text-xs"
>Tipo de factura <span class="text-red-500">*</span></Label
>
@@ -174,21 +174,31 @@
</div>
{#if invoiceType !== 'MEX'}
<div class="col-span-1 flex flex-col items-center space-y-1 pb-1">
<div class="flex flex-col items-center space-y-1 pb-1">
<Label for="is_pedimento_pending" class="text-xs">Pedimento Pendiente?</Label>
<Switch
id="is_pedimento_pending"
checked={formData.is_pedimento_pending}
onCheckedChange={(checked) => {
formData.is_pedimento_pending = checked;
if (checked) {
formData.pedimento_id = null;
formData.pedimento = '';
formData.remesa = '';
formData.fecha_pedimento_del = '';
formData.fecha_pedimento_al = '';
formData.clave_pedimento = '';
formData.regimen_pedimento = '';
}
}}
/>
</div>
<div class="col-span-2 space-y-1">
<div class="min-w-[75px] flex-[2] space-y-1">
<Label for="pedimento" class="text-xs">Pedimento</Label>
<Select.Root
<Select.Root
type="single"
value={formData.pedimento_id ? String(formData.pedimento_id) : ''}
disabled={formData.is_pedimento_pending}
onValueChange={(v) => {
formData.pedimento_id = v ? parseInt(v) : null;
if (v) {
@@ -214,13 +224,13 @@
</Select.Root>
</div>
<div class="col-span-1 space-y-1">
<div class="min-w-[80px] flex-1 space-y-1">
<Label for="remesa" class="text-xs">Remesa</Label>
<Input id="remesa" bind:value={formData.remesa} class="h-8 text-sm" />
</div>
{/if}
<div class="col-span-2 space-y-1">
<div class="min-w-[140px] flex-[2] space-y-1">
<Label for="invoice_number" class="text-xs"
>Núm. Factura <span class="text-red-500">*</span></Label
>
@@ -232,7 +242,7 @@
/>
</div>
<div class="col-span-2 space-y-1">
<div class="min-w-[140px] flex-[2] space-y-1">
<Label for="invoice_date" class="text-xs">
{formData.operation_type === 'exp' || invoiceType === 'CR'
? 'Fecha'
@@ -244,17 +254,17 @@
<Input id="invoice_date" type="date" bind:value={formData.invoice_date} class="h-8 text-sm" />
</div>
<div class="col-span-2 space-y-1">
<div class="min-w-[140px] flex-[2] space-y-1">
<Label for="emission_date" class="text-xs">Fecha Emisión</Label>
<Input id="emission_date" type="date" bind:value={formData.emission_date} class="h-8 text-sm" />
</div>
{#if invoiceType === 'MEX'}
<div class="col-span-1 space-y-1">
<div class="min-w-[90px] flex-1 space-y-1">
<Label for="iva_factor" class="text-xs">Factor IVA</Label>
<Input id="iva_factor" bind:value={formData.iva_factor} class="h-8 text-sm" />
</div>
<div class="col-span-2 space-y-1">
<div class="min-w-[140px] flex-[2] space-y-1">
<Label for="alternate_invoice" class="text-xs">Factura Alterna</Label>
<Input id="alternate_invoice" bind:value={formData.alternate_invoice} class="h-8 text-sm" />
</div>

View File

@@ -96,7 +96,7 @@
}
</script>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Left Column: Observations and Catalog Info -->
<div class="space-y-4">
<!-- Observations Section -->
@@ -249,7 +249,7 @@
{#if operationType === 1 || invoiceType === 'CR'}
<!-- Precintos & Tipo Mov Group (No Title) -->
<div class="grid grid-cols-2 gap-4 rounded-md border bg-muted/20 p-3">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 rounded-md border bg-muted/20 p-3">
<div class="space-y-1">
<Label for="num_seals_exp" class="text-xs font-semibold">Número de Precinto:</Label>
<Select.Root
@@ -290,7 +290,7 @@
<h4 class="mb-1 border-b pb-2 text-xs font-bold text-muted-foreground uppercase">
Factura Alterna & Flags
</h4>
<div class="grid grid-cols-2 gap-x-6 gap-y-4">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4">
<div class="space-y-1.5">
<Label for="alternate_invoice_exp" class="text-xs font-semibold">Factura Alterna:</Label
>
@@ -520,8 +520,8 @@
<!-- Extra Bottom Row for Import Mode specific fields (Original Layout preserved for Import) -->
{#if operationType !== 1 && invoiceType !== 'MEX' && invoiceType !== 'CR'}
<div class="col-span-2 space-y-3 rounded-md border p-3">
<div class="grid grid-cols-4 gap-3">
<div class="col-span-1 md:col-span-2 space-y-3 rounded-md border p-3">
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div class="space-y-1">
<Label for="num_seals" class="text-xs">Num Precintos:</Label>
<Select.Root

View File

@@ -107,7 +107,7 @@
</script>
{#if invoiceType !== 'MEX'}
<div class="grid grid-cols-3 grid-rows-1 gap-3">
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
<div class="space-y-3 rounded-md border p-3">
<!-- Modo de Transporte -->
<div class="space-y-2">
@@ -152,7 +152,7 @@
<RadioGroup.Root
value={String(formData.is_mixed)}
onValueChange={(v: string | undefined) => (formData.is_mixed = v === 'true')}
class="flex gap-4"
class="flex flex-wrap gap-4"
>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="true" id="mixed-si" class="h-4 w-4" />
@@ -248,7 +248,7 @@
</div>
</div>
<div class="col-span-2 space-y-3 rounded-md border p-3">
<div class="col-span-1 md:col-span-2 space-y-3 rounded-md border p-3">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<!-- ID Relación Docs -->
<div class="space-y-2">

View File

@@ -1002,7 +1002,7 @@
}}
/>
<div
class="fixed right-0 bottom-0 left-0 z-[5] ml-[calc(var(--sidebar-width))] border-t bg-background/95 shadow-lg backdrop-blur group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] supports-[backdrop-filter]:bg-background/80"
class="fixed right-0 bottom-0 left-0 z-[5] md:ml-[calc(var(--sidebar-width))] border-t bg-background/95 shadow-lg backdrop-blur md:group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] supports-[backdrop-filter]:bg-background/80"
>
<div class="mx-auto max-w-[1400px] space-y-4 px-4 py-4">
<!-- Tabs Navigation -->