Formularios para cada uno de los tipos de importacion

This commit is contained in:
2026-02-11 10:27:32 -06:00
parent 4190ac891e
commit ac47a99f1a
23 changed files with 1093 additions and 617 deletions

View File

@@ -14,7 +14,7 @@ def validate_create(db: Session, invoice: InvoiceHeaderCreate, tenant_id: int, c
if not invoice.invoice_type:
errors.add_required_error("invoice_type")
if not invoice.document_type:
if not invoice.document_type and invoice.invoice_type != "MEX":
errors.add_required_error("document_type")
if not invoice.invoice_number:

View File

@@ -224,9 +224,10 @@ def validate_update(
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")
# Validar que aduana sea obligatorio (excepto para MEX)
if existing_invoice.invoice_type != "MEX":
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:

View File

@@ -58,7 +58,7 @@ class TransportType(str, Enum):
PLATES = "licence plates"
TRUCK = "truck"
VESSEL = "vessel"
BARGE = "rail barge"
BARGE = "rail_barge"
CONTAINER = "container"
AIRPLANE = "airplane"
GONDOLA = "gondola"
@@ -82,9 +82,10 @@ class InvoiceHeader(Base, TenantScopedMixin, TimestampMixin):
invoice_type: Mapped[str] = mapped_column(
ForeignKey("public.invoice_types.key")
) # TIPOFACTURA / TIPODOC
document_type: Mapped[str] = mapped_column(
ForeignKey("public.pedimento_regimens.code")
) # CLAVEDOCUMENTO / Clave de documento
document_type: Mapped[Optional[str]] = mapped_column(
ForeignKey("public.pedimento_regimens.code"), nullable=True
)
# CLAVEDOCUMENTO / Clave de documento
invoice_number: Mapped[str] = mapped_column(
String(100)
) # FACTURAIMPO/FACTURAEXPO/FACTURAREMISION/FACTURAENVIO/FACTURASALIDA
@@ -299,6 +300,9 @@ class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin):
act_value: Mapped[Optional[str]] = mapped_column(
String(5)
) # ACTVALOR / Actualizar valor
rule_3121_parties_ii: Mapped[Optional[bool]] = mapped_column(
Boolean, default=False, server_default="false"
) # REGLA3121PARTESII / Regla 3.1.21 Partes II
is_pedimento_pending: Mapped[Optional[bool]] = mapped_column(
Boolean, default=False, server_default="false"
) # PED_PENDIENTE_ASIGNAR (Mapear 1 -> True, 0 -> False)
@@ -375,7 +379,7 @@ class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin):
String(1)
) # RAZONEXPORTACION / Razón de exportación
signature_key: Mapped[Optional[str]] = mapped_column(
String(10)
String(100)
) # CLAVEFIRMA / Clave de firma
# SM specific

View File

@@ -22,8 +22,8 @@ class InvoiceHeaderBase(BaseModel):
invoice_type: Optional[str] = Field(
None, max_length=5, description="Invoice type key"
)
document_type: str = Field(
..., max_length=3, description="Document type (Regimen Aduanero)"
document_type: Optional[str] = Field(
None, max_length=3, description="Document type (Regimen Aduanero)"
)
invoice_number: Optional[str] = Field(
None, max_length=100, description="Invoice number"
@@ -139,6 +139,9 @@ class InvoiceComplianceMxBase(BaseModel):
which_exchange_rate: Optional[str] = Field(
None, max_length=5, description="Which exchange rate"
)
rule_3121_parties_ii: Optional[bool] = Field(
False, description="Is Rule 3.1.21 Parties II"
)
value_method: Optional[str] = Field(None, max_length=2, description="Value method")
act_value: Optional[str] = Field(None, max_length=5, description="Act value")
is_pedimento_pending: bool = Field(..., description="Is pedimento pending")
@@ -187,7 +190,7 @@ class InvoiceComplianceMxBase(BaseModel):
None, max_length=1, description="Reason for export"
)
signature_key: Optional[str] = Field(
None, max_length=10, description="Signature key"
None, max_length=100, description="Signature key"
)
sem_id: Optional[int] = Field(None, description="SEM ID")

View File

@@ -128,6 +128,10 @@ class InvoiceService:
invoice_dict = clean_dict(raw_invoice_dict)
invoice_dict["tenant_id"] = tenant_id
invoice_dict["company_id"] = company_id
# Ensure document_type respects DB constraints for MEX invoices (bypass clean_dict)
if invoice_dict.get("invoice_type") == "MEX" and not invoice_dict.get("document_type"):
invoice_dict["document_type"] = None
new_invoice = models.InvoiceHeader(**invoice_dict)

View File

@@ -28,6 +28,13 @@ seed = [
"both",
"imp",
),
(
"REP",
"REPARACION",
"REPARACION DE ACTIVOS FIJOS",
"fixed asset",
"imp",
),
# === TIPOS DE EXPORTACION ===
("DONAC", "DONACION", "", "both", "exp"),

View File

@@ -0,0 +1,35 @@
from core.database import CoreSessionLocal
from api.v1.modules.public.reference_data.invoice_types.models import InvoiceType
from api.v1.modules.public.reference_data.invoice_types.seed import seed
def sync_invoice_types():
db = CoreSessionLocal()
try:
for s in seed:
key, description, note, type_, operation = s
obj = db.query(InvoiceType).filter(InvoiceType.key == key).first()
if obj:
print(f"Updating {key}...")
obj.description = description
obj.note = note
obj.type = type_
obj.operation = operation
else:
print(f"Creating {key}...")
obj = InvoiceType(
key=key,
description=description,
note=note,
type=type_,
operation=operation
)
db.add(obj)
db.commit()
except Exception as e:
print(f"Error: {e}")
db.rollback()
finally:
db.close()
if __name__ == "__main__":
sync_invoice_types()

View File

@@ -82,7 +82,8 @@
"temporary": "Temporary",
"definitive": "Definitive",
"mexican_purchases": "Mexican Purchases",
"regime_change": "Regime Change"
"regime_change": "Regime Change",
"repair": "Repair"
},
"export_invoices": {
"title": "Export Invoices",

View File

@@ -82,7 +82,8 @@
"temporary": "Temporal",
"definitive": "Definitiva",
"mexican_purchases": "Compras mexicanas",
"regime_change": "Cambio de régimen"
"regime_change": "Cambio de régimen",
"repair": "Reparación"
},
"export_invoices": {
"title": "Facturas de exportación",

View File

@@ -73,6 +73,7 @@ export interface InvoiceComplianceMx {
reason_export?: string | null;
signature_key?: string | null;
sem_id?: number | null;
rule_3121_parties_ii?: boolean | null;
}
export interface InvoiceFinancials {

View File

@@ -269,7 +269,7 @@
// Helper to check if Repar request
const isRepar = (t: string | undefined) => {
return t && t.toUpperCase().includes('REPAR');
return t === 'REPAR';
};
// Categorize

View File

@@ -0,0 +1,108 @@
<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Search, LoaderCircle, X, Check } from 'lucide-svelte';
import {
customsSectionsApi,
type CustomsSection
} from '$lib/api/dashboard/reference_data/customs_sections';
import { toast } from 'svelte-sonner';
let { open = $bindable(false), onSelect } = $props<{
open: boolean;
onSelect: (section: CustomsSection) => void;
}>();
let searchQuery = $state('');
let sections = $state<CustomsSection[]>([]);
let loading = $state(false);
let selectedSection = $state<CustomsSection | null>(null);
async function loadSections() {
loading = true;
try {
const response = await customsSectionsApi.list(1, 100);
if (response?.data?.items) {
sections = response.data.items;
}
} catch (error) {
console.error('Error loading customs sections:', error);
toast.error('Error al cargar las secciones aduaneras');
} finally {
loading = false;
}
}
$effect(() => {
if (open) {
loadSections();
}
});
const filteredSections = $derived(
sections.filter(
(s) =>
s.customs_code.toLowerCase().includes(searchQuery.toLowerCase()) ||
s.section_name.toLowerCase().includes(searchQuery.toLowerCase())
)
);
function handleSelect(section: CustomsSection) {
onSelect(section);
open = false;
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="max-w-2xl">
<Dialog.Header>
<Dialog.Title>Seleccionar Puerto (Aduana/Sección)</Dialog.Title>
<Dialog.Description>Busca y selecciona una sección aduanera de la lista.</Dialog.Description>
</Dialog.Header>
<div class="space-y-4 py-4">
<div class="relative">
<Search class="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input placeholder="Buscar por código o nombre..." class="pl-10" bind:value={searchQuery} />
</div>
<div class="max-h-[400px] overflow-y-auto rounded-md border">
{#if loading}
<div class="flex items-center justify-center py-10">
<LoaderCircle class="h-8 w-8 animate-spin text-primary" />
</div>
{:else if filteredSections.length > 0}
<table class="w-full text-sm">
<thead class="sticky top-0 bg-muted">
<tr>
<th class="p-2 text-left font-medium">Código</th>
<th class="p-2 text-left font-medium">Nombre / Sección</th>
<th class="w-10 p-2"></th>
</tr>
</thead>
<tbody class="divide-y">
{#each filteredSections as section}
<tr class="cursor-pointer hover:bg-muted/50" onclick={() => handleSelect(section)}>
<td class="p-2 font-mono">{section.customs_code}</td>
<td class="p-2">{section.section_name}</td>
<td class="p-2">
<Button variant="ghost" size="icon" class="h-8 w-8">
<Check class="h-4 w-4 text-primary" />
</Button>
</td>
</tr>
{/each}
</tbody>
</table>
{:else}
<div class="py-10 text-center text-muted-foreground">No se encontraron resultados</div>
{/if}
</div>
</div>
<Dialog.Footer>
<Button variant="outline" onclick={() => (open = false)}>Cancelar</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -4,18 +4,22 @@
import { Checkbox } from '$lib/components/ui/checkbox';
import { RadioGroup, RadioGroupItem } from '$lib/components/ui/radio-group';
import { Button } from '$lib/components/ui/button';
import { Search } from 'lucide-svelte';
import PortSelectorModal from './PortSelectorModal.svelte';
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
let {
invoice,
formData = $bindable(),
exists = $bindable(),
operationType = undefined
operationType = undefined,
invoiceType = undefined
}: {
invoice: Invoice | null;
formData?: any;
exists?: boolean;
operationType?: number;
invoiceType?: string;
} = $props();
if (!formData && invoice) {
@@ -28,6 +32,7 @@
cantidad_guias_embarque: invoice.logistics?.guide_number || null,
destino_origen: invoice.logistics?.destination_location || '',
puerto_entrada: invoice.logistics?.origin_location || '',
vehicle_data: invoice.logistics?.vehicle_data || '',
// Checkboxes
fue_revisado_equipo: invoice.logistics?.equipment_reviewed || false,
sub_division: invoice.logistics?.is_subdivision || false,
@@ -60,6 +65,7 @@
cantidad_guias_embarque: null,
destino_origen: '',
puerto_entrada: '',
vehicle_data: '',
// Checkboxes
fue_revisado_equipo: false,
sub_division: false,
@@ -84,6 +90,11 @@
};
exists = false;
}
let showPortModal = $state(false);
function handlePortSelect(section: any) {
formData.puerto_entrada = section.customs_code;
}
</script>
<!-- Layout de 2 columnas compacto -->
@@ -104,7 +115,8 @@
<!-- DATOS VEHÍCULO -->
<div class="space-y-2 rounded border bg-muted/30 p-2">
<Label class="text-xs font-semibold">Datos Vehículo:</Label>
<Label for="vehicle_data" class="text-xs font-semibold">Datos Vehículo:</Label>
<Input id="vehicle_data" bind:value={formData.vehicle_data} class="mb-2 h-7 text-xs" />
<div class="space-y-1.5">
<Label class="text-xs">Es Ferrocarril?</Label>
<RadioGroup bind:value={formData.es_ferrocarril} class="flex gap-4">
@@ -147,7 +159,7 @@
/>
</div>
{#if operationType === 1}
{#if operationType === 1 || invoiceType === 'CR' || invoiceType === 'REP'}
<div class="space-y-1.5">
<Label class="text-xs">Es Mixto?</Label>
<RadioGroup bind:value={formData.is_mixed} class="flex gap-4">
@@ -164,14 +176,28 @@
{/if}
</div>
{#if operationType !== 1}
{#if operationType !== 1 && invoiceType !== 'CR'}
<div class="space-y-1.5">
<Label for="puerto_entrada" class="text-xs">Puerto Entrada:</Label>
<Input id="puerto_entrada" bind:value={formData.puerto_entrada} class="h-7 text-xs" />
<div class="flex gap-2">
<Input
id="puerto_entrada"
bind:value={formData.puerto_entrada}
class="h-7 flex-1 text-xs"
/>
<Button
variant="outline"
size="icon"
class="h-7 w-7"
onclick={() => (showPortModal = true)}
>
<Search class="h-3.5 w-3.5" />
</Button>
</div>
</div>
{/if}
{#if operationType === 1}
{#if operationType === 1 || invoiceType === 'CR'}
<div class="space-y-3 pt-1">
<div class="space-y-1.5">
<Label class="text-xs">Razón de exportación:</Label>
@@ -216,25 +242,25 @@
<!-- CHECKBOXES -->
<div class="grid grid-cols-2 gap-2">
{#if operationType !== 1}
{#if operationType !== 1 && invoiceType !== 'CR'}
<div class="flex items-center gap-2">
<Checkbox id="fue_revisado" bind:checked={formData.fue_revisado_equipo} />
<Label for="fue_revisado" class="text-xs">Fue Revisado el Equipo</Label>
</div>
<div class="flex items-center gap-2">
<Checkbox id="sub_division" bind:checked={formData.sub_division} />
<Label for="sub_division" class="text-xs">Sub División</Label>
</div>
<div class="flex items-center gap-2">
<Checkbox id="funge_como_cd" bind:checked={formData.funge_como_cd} />
<Label for="funge_como_cd" class="text-xs">Funge Como CD</Label>
</div>
{/if}
{#if operationType !== 1}
<div class="flex items-center gap-2">
<Checkbox id="llego_pedimento" bind:checked={formData.llego_pedimento} />
<Label for="llego_pedimento" class="text-xs">Llegó el Pedimento</Label>
</div>
{#if invoiceType !== 'REP' && invoiceType !== 'REPAR'}
<div class="flex items-center gap-2">
<Checkbox id="sub_division" bind:checked={formData.sub_division} />
<Label for="sub_division" class="text-xs">Sub División</Label>
</div>
<div class="flex items-center gap-2">
<Checkbox id="funge_como_cd" bind:checked={formData.funge_como_cd} />
<Label for="funge_como_cd" class="text-xs">Funge Como CD</Label>
</div>
<div class="flex items-center gap-2">
<Checkbox id="llego_pedimento" bind:checked={formData.llego_pedimento} />
<Label for="llego_pedimento" class="text-xs">Llegó el Pedimento</Label>
</div>
{/if}
{/if}
</div>
</div>
@@ -305,7 +331,7 @@
</div>
</div>
{#if operationType === 1}
{#if operationType === 1 || invoiceType === 'CR'}
<!-- CFDI Section (Export Only) -->
<div class="space-y-3 rounded border bg-muted/20 p-3">
<h4 class="border-b pb-1 text-[10px] font-bold text-muted-foreground uppercase">
@@ -327,3 +353,5 @@
{/if}
</div>
</div>
<PortSelectorModal bind:open={showPortModal} onSelect={handlePortSelect} />

View File

@@ -4,7 +4,7 @@
import * as Select from '$lib/components/ui/select';
import * as RadioGroup from '$lib/components/ui/radio-group';
import { Button } from '$lib/components/ui/button';
import { Search } from 'lucide-svelte';
import { Search, Upload } from 'lucide-svelte';
import ManifestSelectorModal from './ManifestSelectorModal.svelte';
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
import type { InvoiceType } from '$lib/api/dashboard/reference_data/invoice_types';
@@ -32,7 +32,8 @@
codePedimentoRegimens = [],
operationType = undefined,
defaultOperationType = undefined,
exchangeRate = undefined
exchangeRate = undefined,
invoiceType = undefined
}: {
invoice: Invoice | null;
formData?: any;
@@ -52,6 +53,7 @@
defaultInvoiceType?: string | null;
operationType?: number | null;
exchangeRate?: number | null;
invoiceType?: string;
} = $props();
let showManifestModal = $state(false);
@@ -96,7 +98,9 @@
transport_num: invoice.logistics?.vehicle_num || '',
aduana: invoice.compliance_mx?.aduana || '',
document_type: invoice.document_type || '',
manifest_number: invoice.compliance_mx?.manifest_number || ''
manifest_number: invoice.compliance_mx?.manifest_number || '',
code_signature: invoice.compliance_mx?.signature_key || '',
electronic_signature: invoice.compliance_mx?.electronic_signature || ''
};
} else {
// Creando una nueva factura
@@ -126,7 +130,9 @@
transport_num: '',
aduana: '',
document_type: '',
manifest_number: ''
manifest_number: '',
code_signature: '',
electronic_signature: ''
};
}
} else {
@@ -176,7 +182,7 @@
];
const soldToHeaderOptions = $derived(
operationType === 1
operationType === 1 || invoiceType === 'CR'
? [
{ value: 'consignado_a', label: 'Consignado a' },
{ value: 'vendido_a', label: 'Vendido a' },
@@ -190,7 +196,7 @@
);
const shippedToHeaderOptions = $derived(
operationType === 1
operationType === 1 || invoiceType === 'CR'
? [
{ value: 'enviado_a', label: 'Enviado a' },
{ value: 'transferido_a', label: 'Transferido a' },
@@ -204,7 +210,7 @@
);
const shippedByHeaderOptions = $derived(
operationType === 1
operationType === 1 || invoiceType === 'CR'
? [
{ value: 'enviado_por', label: 'Enviado Por' },
{ value: 'vendido_por', label: 'Vendido Por' },
@@ -296,25 +302,27 @@
<!-- Columna Izquierda: Clientes - Proveedores - Agente Aduanal -->
<div class="space-y-3 rounded-md border p-3">
<div class="space-y-2">
<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>
<span class="text-muted-foreground">Fecha del:</span>
<p class="font-medium">{formData.fecha_pedimento_del || '-'}</p>
{#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>
<span class="text-muted-foreground">Fecha del:</span>
<p class="font-medium">{formData.fecha_pedimento_del || '-'}</p>
</div>
<div>
<span class="text-muted-foreground">Fecha al:</span>
<p class="font-medium">{formData.fecha_pedimento_al || '-'}</p>
</div>
<div>
<span class="text-muted-foreground">Clave:</span>
<p class="font-medium">{formData.clave_pedimento || '-'}</p>
</div>
<div>
<span class="text-muted-foreground">Régimen:</span>
<p class="font-medium">{formData.regimen_pedimento || '-'}</p>
</div>
</div>
<div>
<span class="text-muted-foreground">Fecha al:</span>
<p class="font-medium">{formData.fecha_pedimento_al || '-'}</p>
</div>
<div>
<span class="text-muted-foreground">Clave:</span>
<p class="font-medium">{formData.clave_pedimento || '-'}</p>
</div>
<div>
<span class="text-muted-foreground">Régimen:</span>
<p class="font-medium">{formData.regimen_pedimento || '-'}</p>
</div>
</div>
{/if}
</div>
<h4 class="text-xs font-semibold text-muted-foreground uppercase">
@@ -471,7 +479,7 @@
<span class="text-red-500">*</span>
</div>
{#if operationType === 1}
{#if operationType === 1 || invoiceType === 'CR'}
<div class="grid grid-cols-4 gap-3 space-y-1.5">
<Select.Root
type="single"
@@ -677,7 +685,7 @@
</Select.Root>
</div>
{#if operationType !== 1}
{#if operationType !== 1 && invoiceType !== 'MEX' && invoiceType !== 'CR' && invoiceType !== 'REP' && invoiceType !== 'REPAR'}
<div class="space-y-1.5">
<Label for="iva_factor" class="text-xs">IVA:</Label>
<Input
@@ -690,7 +698,7 @@
/>
</div>
{/if}
{#if operationType === 1}
{#if operationType === 1 || invoiceType === 'CR'}
<div class="space-y-1.5">
<Label for="manifest_number" class="text-xs">Num. de Manifiesto:</Label>
<div class="flex gap-2">
@@ -719,37 +727,39 @@
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Transportista</h4>
<div class="grid grid-cols-3 gap-2">
<div class="space-y-1.5">
<Label for="carrier_id" class="text-xs">Transportista:</Label>
<Select.Root
type="single"
value={formData.carrier_id ? String(formData.carrier_id) : ''}
onValueChange={(v) => {
formData.carrier_id = v || null;
}}
>
<Select.Trigger id="carrier_id" class="h-7 max-w-[250px] min-w-[120px] text-xs">
<span class="truncate">
{#if formData.carrier_id}
{transporters.find(
(t) => String(t.transporter_key) === String(formData.carrier_id)
)?.name || formData.carrier_id}
{:else if transporters.length > 0}
Selecciona transportista...
{:else}
Sin datos
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each transporters as transporter}
<Select.Item value={String(transporter.transporter_key)}>
{transporter.transporter_key}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
{#if invoiceType !== 'MEX'}
<div class="space-y-1.5">
<Label for="carrier_id" class="text-xs">Transportista:</Label>
<Select.Root
type="single"
value={formData.carrier_id ? String(formData.carrier_id) : ''}
onValueChange={(v) => {
formData.carrier_id = v || null;
}}
>
<Select.Trigger id="carrier_id" class="h-7 max-w-[250px] min-w-[120px] text-xs">
<span class="truncate">
{#if formData.carrier_id}
{transporters.find(
(t) => String(t.transporter_key) === String(formData.carrier_id)
)?.name || formData.carrier_id}
{:else if transporters.length > 0}
Selecciona transportista...
{:else}
Sin datos
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each transporters as transporter}
<Select.Item value={String(transporter.transporter_key)}>
{transporter.transporter_key}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
{/if}
<div class="col-span-2 space-y-1.5">
<Label for="transport_id" class="text-xs">Clave Transporte:</Label>
@@ -872,71 +882,91 @@
</div>
</div>
<div class="space-y-1.5">
<Label for="aduana" class="text-xs">Aduana y Sección de Despacho:</Label>
<Select.Root
type="single"
value={formData.aduana || ''}
onValueChange={(v) => {
formData.aduana = v ?? '';
}}
>
<Select.Trigger id="aduana" class="h-7 w-full text-xs">
<span class="truncate">
{#if formData.aduana}
{customsSections.find((cs) => cs.customs_code === formData.aduana)?.section_name ||
formData.aduana}
{:else if customsSections.length > 0}
Selecciona aduana...
{:else}
Sin datos
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each customsSections as section}
<Select.Item value={section.customs_code}>
{section.customs_code} - {section.section_name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
{#if invoiceType !== 'MEX'}
<div class="space-y-1.5">
<Label for="aduana" class="text-xs">Aduana y Sección de Despacho:</Label>
<Select.Root
type="single"
value={formData.aduana || ''}
onValueChange={(v) => {
formData.aduana = v ?? '';
}}
>
<Select.Trigger id="aduana" class="h-7 w-full text-xs">
<span class="truncate">
{#if formData.aduana}
{customsSections.find((cs) => cs.customs_code === formData.aduana)
?.section_name || formData.aduana}
{:else if customsSections.length > 0}
Selecciona aduana...
{:else}
Sin datos
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each customsSections as section}
<Select.Item value={section.customs_code}>
{section.customs_code} - {section.section_name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
{/if}
<div class="space-y-1.5">
<Label for="document_type" class="text-xs"
>Clave de Régimen Aduanero: <span class="text-red-500">*</span></Label
>
<Select.Root
type="single"
value={formData.document_type || ''}
onValueChange={(v) => {
formData.document_type = v ?? '';
}}
>
<Select.Trigger id="document_type" class="h-7 w-full text-xs">
<span class="truncate">
{#if formData.document_type}
{codePedimentoRegimens.find((r) => r.regimen_code === formData.document_type)
?.regimen_code || formData.document_type}
{:else if filteredRegimens.length > 0}
Selecciona régimen...
{:else if operationType}
Sin regímenes para tipo {operationType}
{:else}
Selecciona tipo de operación primero
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each filteredRegimens as regimen}
<Select.Item value={regimen.regimen_code}>
{regimen.regimen_code}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
{#if invoiceType !== 'MEX'}
<div class="space-y-1.5">
<Label for="document_type" class="text-xs"
>Clave de Régimen Aduanero: <span class="text-red-500">*</span></Label
>
<Select.Root
type="single"
value={formData.document_type || ''}
onValueChange={(v) => {
formData.document_type = v ?? '';
}}
>
<Select.Trigger id="document_type" class="h-7 w-full text-xs">
<span class="truncate">
{#if formData.document_type}
{codePedimentoRegimens.find((r) => r.regimen_code === formData.document_type)
?.regimen_code || formData.document_type}
{:else if filteredRegimens.length > 0}
Selecciona régimen...
{:else if operationType}
Sin regímenes para tipo {operationType}
{:else}
Selecciona tipo de operación primero
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each filteredRegimens as regimen}
<Select.Item value={regimen.regimen_code}>
{regimen.regimen_code}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
{/if}
{#if invoiceType === 'MEX'}
<div class="space-y-1.5 pt-1">
<Label for="electronic-sig-gen" class="text-xs">Firma Electrónica:</Label>
<div class="flex gap-2">
<Input
id="electronic-sig-gen"
bind:value={formData.code_signature}
class="h-7 flex-1 text-xs"
/>
<Button variant="outline" size="icon" class="h-7 w-7">
<Upload class="h-3 w-3" />
</Button>
</div>
</div>
{/if}
</div>
</div>
</div>

View File

@@ -13,7 +13,8 @@
invoiceTypes = [],
pedimentos = [],
defaultOperationType = undefined,
defaultInvoiceType = undefined
defaultInvoiceType = undefined,
invoiceType = undefined
}: {
invoice: Invoice | null;
formData?: any;
@@ -21,6 +22,7 @@
pedimentos?: Pedimento[];
defaultOperationType?: string | null;
defaultInvoiceType?: string | null;
invoiceType?: string;
} = $props();
function handlePedimentoChange(pedimentoId: string) {
@@ -81,7 +83,10 @@
fecha_pedimento_del: '',
fecha_pedimento_al: '',
clave_pedimento: '',
regimen_pedimento: ''
regimen_pedimento: '',
// Campos específicos para MEX
iva_factor: invoice?.financials?.iva_factor || '',
alternate_invoice: invoice?.alternate_invoice || ''
};
} else {
// Si formData ya existe pero operation_type está vacío, usar defaultOperationType
@@ -92,7 +97,29 @@
) {
formData.operation_type = defaultOperationType;
}
// Ensure new fields exist if formData was created before
if (formData.iva_factor === undefined)
formData.iva_factor = invoice?.financials?.iva_factor || '';
if (formData.alternate_invoice === undefined)
formData.alternate_invoice = invoice?.alternate_invoice || '';
}
// Filter invoice types based on operation type
let filteredInvoiceTypes = $derived(
invoiceTypes.filter((type) => {
if (!formData.operation_type) return true;
return type.operation === 'both' || type.operation === formData.operation_type;
})
);
// Reset invoice_type if not valid for new operation_type
$effect(() => {
if (formData.operation_type && formData.invoice_type) {
const isValid = filteredInvoiceTypes.some((t) => t.key === formData.invoice_type);
if (!isValid) {
formData.invoice_type = '';
}
}
});
</script>
<!-- Datos Principales en una fila compacta (reusable across tabs) -->
@@ -136,7 +163,7 @@
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each invoiceTypes as type}
{#each filteredInvoiceTypes as type}
<Select.Item value={type.key}>
{type.key} - {type.description}
</Select.Item>
@@ -145,47 +172,52 @@
</Select.Root>
</div>
<div class="col-span-1 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;
}}
/>
</div>
<div class="col-span-2 space-y-1">
<Label for="pedimento" class="text-xs">Pedimento</Label>
<Select.Root
type="single"
value={formData.pedimento_id ? String(formData.pedimento_id) : ''}
onValueChange={(v) => {
formData.pedimento_id = v ? parseInt(v) : null;
if (v) {
handlePedimentoChange(v);
}
}}
>
<Select.Trigger id="pedimento" class="h-8 text-sm">
<span class="truncate">
{formData.pedimento || 'Selecciona pedimento...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each pedimentos as pedimento}
<Select.Item value={String(pedimento.id)}>
{pedimento.customs_office?.slice(0, 2)}-{pedimento.license}-{pedimento.pedimento_number}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
{#if invoiceType !== 'MEX'}
<div class="col-span-1 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;
}}
/>
</div>
<div class="col-span-2 space-y-1">
<Label for="pedimento" class="text-xs">Pedimento</Label>
<Select.Root
type="single"
value={formData.pedimento_id ? String(formData.pedimento_id) : ''}
onValueChange={(v) => {
formData.pedimento_id = v ? parseInt(v) : null;
if (v) {
handlePedimentoChange(v);
}
}}
>
<Select.Trigger id="pedimento" class="h-8 text-sm">
<span class="truncate">
{formData.pedimento || 'Selecciona pedimento...'}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each pedimentos as pedimento}
<Select.Item value={String(pedimento.id)}>
{pedimento.customs_office?.slice(
0,
2
)}-{pedimento.license}-{pedimento.pedimento_number}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="col-span-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>
<div class="col-span-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">
<Label for="invoice_number" class="text-xs"
@@ -201,7 +233,11 @@
<div class="col-span-2 space-y-1">
<Label for="invoice_date" class="text-xs">
{formData.operation_type === 'exp' ? 'Fecha' : 'Fecha Factura'}
{formData.operation_type === 'exp' || invoiceType === 'CR'
? 'Fecha'
: invoiceType === 'MEX'
? 'Fecha de Entrada'
: 'Fecha Factura'}
<span class="text-red-500">*</span>
</Label>
<Input id="invoice_date" type="date" bind:value={formData.invoice_date} class="h-8 text-sm" />
@@ -211,4 +247,15 @@
<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">
<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">
<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>
{/if}
</div>

View File

@@ -16,12 +16,14 @@
invoice,
formData = $bindable(),
exists = $bindable(),
operationType = undefined
operationType = undefined,
invoiceType = undefined
}: {
invoice: Invoice | null;
formData?: any;
exists?: boolean;
operationType?: number;
invoiceType?: string;
} = $props();
let imported = 0;
@@ -258,10 +260,10 @@
}
// Load part number data
if (line.part_number_id) {
if (line.part_number) {
try {
const response = await fetch(
`/api-sveltekit/parts/${line.part_number_id}?company_id=${activeCompanyId}`,
`/api-sveltekit/parts/${line.part_number}?company_id=${activeCompanyId}`,
{ method: 'GET', headers: { 'Content-Type': 'application/json' } }
);
if (response.ok) {
@@ -733,6 +735,30 @@
<Table.Head>Descripción Esp.</Table.Head>
<Table.Head>Subpartidas</Table.Head>
<Table.Head>Partida Princ.</Table.Head>
{:else if invoiceType === 'CR'}
<Table.Head>Línea</Table.Head>
<Table.Head>Factura Impo</Table.Head>
<Table.Head>Lin Impo</Table.Head>
<Table.Head>P/S</Table.Head>
<Table.Head>Cantidad Exportada</Table.Head>
<Table.Head>Clase</Table.Head>
<Table.Head>Número de Parte</Table.Head>
<Table.Head>Descripción en Español</Table.Head>
<Table.Head>Contiene Subpartida</Table.Head>
<Table.Head>Partida Principal</Table.Head>
{:else if invoiceType === 'REP' || invoiceType === 'REPAR'}
<Table.Head>Línea</Table.Head>
<Table.Head>No. Factura Expo</Table.Head>
<Table.Head>Línea Expo</Table.Head>
<Table.Head>P/S</Table.Head>
<Table.Head>Clase</Table.Head>
<Table.Head>Num. Parte</Table.Head>
<Table.Head>Descripción en Español</Table.Head>
<Table.Head>Cantidad Importada</Table.Head>
<Table.Head>UM</Table.Head>
<Table.Head>Preferencia</Table.Head>
<Table.Head>Contiene Subpartida</Table.Head>
<Table.Head>Partida Principal</Table.Head>
{:else}
<Table.Head>Línea</Table.Head>
<Table.Head>P/S</Table.Head>
@@ -782,6 +808,40 @@
</Table.Cell>
<Table.Cell>{item.fa_data?.contains_subitems ? 'Sí' : 'No'}</Table.Cell>
<Table.Cell>{item.warehouse || '-'}</Table.Cell>
{:else if invoiceType === 'CR'}
<Table.Cell>{item.line_number}</Table.Cell>
<Table.Cell>{item.fa_data?.search_invoice || '-'}</Table.Cell>
<Table.Cell>{item.fa_data?.search_line || '-'}</Table.Cell>
<Table.Cell>{item.is_subitem ? 'S' : 'P'}</Table.Cell>
<Table.Cell>{item.quantity?.quantity || '0'}</Table.Cell>
<Table.Cell>{item.class_code || '-'}</Table.Cell>
<Table.Cell>{item.part_number || '-'}</Table.Cell>
<Table.Cell
class="max-w-[200px] truncate"
title={item.description?.description_spanish}
>
{item.description?.description_spanish || '-'}
</Table.Cell>
<Table.Cell>{item.fa_data?.contains_subitems ? 'Sí' : 'No'}</Table.Cell>
<Table.Cell>{item.warehouse || '-'}</Table.Cell>
{:else if invoiceType === 'REP' || invoiceType === 'REPAR'}
<Table.Cell>{item.line_number}</Table.Cell>
<Table.Cell>{item.fa_data?.search_invoice || '-'}</Table.Cell>
<Table.Cell>{item.fa_data?.search_line || '-'}</Table.Cell>
<Table.Cell>{item.is_subitem ? 'S' : 'P'}</Table.Cell>
<Table.Cell>{item.class_code || '-'}</Table.Cell>
<Table.Cell>{item.part_number || '-'}</Table.Cell>
<Table.Cell
class="max-w-[200px] truncate"
title={item.description?.description_spanish}
>
{item.description?.description_spanish || '-'}
</Table.Cell>
<Table.Cell>{item.quantity?.quantity || '0'}</Table.Cell>
<Table.Cell>{item.unit_of_measure_code || '-'}</Table.Cell>
<Table.Cell>{item.reference_number || '-'}</Table.Cell>
<Table.Cell>{item.fa_data?.contains_subitems ? 'Sí' : 'No'}</Table.Cell>
<Table.Cell>{item.warehouse || '-'}</Table.Cell>
{:else}
<Table.Cell>{item.line_number}</Table.Cell>
<Table.Cell>{item.is_subitem ? 'S' : 'P'}</Table.Cell>

View File

@@ -12,7 +12,8 @@
seals = [],
incoterms = [],
enclosure = [],
operationType = undefined
operationType = undefined,
invoiceType = undefined
}: {
invoice: Invoice | null;
formData?: any;
@@ -21,6 +22,7 @@
incoterms?: any[];
enclosure?: any[];
operationType?: number;
invoiceType?: string;
} = $props();
if (!formData && invoice) {
@@ -53,7 +55,8 @@
complement_1: invoice.logistics?.complement_1 || '',
identifier_2: invoice.logistics?.identifier_2 || '',
complement_2: invoice.logistics?.complement_2 || '',
office_document: invoice.compliance_mx?.office_document || ''
office_document: invoice.compliance_mx?.office_document || '',
is_mixed: invoice.compliance_mx?.is_mixed || false
};
exists = true;
} else if (!formData) {
@@ -86,7 +89,8 @@
complement_1: '',
identifier_2: '',
complement_2: '',
office_document: ''
office_document: '',
is_mixed: false
};
exists = false;
}
@@ -99,9 +103,11 @@
<div class="space-y-4 rounded-md border p-3">
<div class="space-y-2">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">
{operationType === 1
{operationType === 1 || invoiceType === 'CR'
? 'Observaciones de la factura mexicana:'
: 'Observacion de la factura mexicana y bilingue:'}
: invoiceType === 'MEX'
? 'Observacion de la factura mexicana y bilingue:'
: 'Observacion de la factura mexicana y bilingue:'}
</h4>
<Textarea
id="observation_es"
@@ -111,17 +117,19 @@
/>
</div>
<div class="space-y-2">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">
Observaciones de la factura americana:
</h4>
<Textarea
id="observation_en"
bind:value={formData.observation_en}
class="max-h-[150px] min-h-[150px] text-sm font-medium"
placeholder="Escribe tus observaciones aqui."
/>
</div>
{#if invoiceType !== 'MEX'}
<div class="space-y-2">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">
Observaciones de la factura americana:
</h4>
<Textarea
id="observation_en"
bind:value={formData.observation_en}
class="max-h-[150px] min-h-[150px] text-sm font-medium"
placeholder="Escribe tus observaciones aqui."
/>
</div>
{/if}
</div>
{#if operationType === 1}
@@ -395,83 +403,123 @@
{/if}
<!-- Incoterm & Recinto (Show for all, layout adjusted) -->
<div class="space-y-3 rounded-md border p-3">
<div class="grid grid-cols-2 gap-4">
<div class="space-y-1">
<Label for="incoterm" class="text-xs">Incoterm:</Label>
<Select.Root
type="single"
value={formData.incoterm ? String(formData.incoterm) : ''}
onValueChange={(v) => {
formData.incoterm = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="incoterm" class="h-7 text-xs">
<span class="truncate"
>{formData.incoterm
? incoterms.find((p) => p.id === formData.incoterm)?.name || 'Selecciona...'
: 'Selecciona...'}</span
>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each incoterms as inco}
<Select.Item value={String(inco.id)}>{inco.name}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
{#if invoiceType !== 'MEX'}
<div class="space-y-3 rounded-md border p-3">
<div class="grid grid-cols-2 gap-4">
<div class="space-y-1">
<Label for="incoterm" class="text-xs">Incoterm:</Label>
<Select.Root
type="single"
value={formData.incoterm ? String(formData.incoterm) : ''}
onValueChange={(v) => {
formData.incoterm = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="incoterm" class="h-7 text-xs">
<span class="truncate"
>{formData.incoterm
? incoterms.find((p) => p.id === formData.incoterm)?.name || 'Selecciona...'
: 'Selecciona...'}</span
>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each incoterms as inco}
<Select.Item value={String(inco.id)}>{inco.name}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1">
<Label for="enclosure" class="text-xs">Recinto:</Label>
<Select.Root
type="single"
value={formData.enclosure ? String(formData.enclosure) : ''}
onValueChange={(v) => {
formData.enclosure = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="enclosure" class="h-7 text-xs">
<span class="truncate"
>{formData.enclosure
? enclosure.find((p) => p.id === formData.enclosure)?.name || 'Selecciona...'
: 'Selecciona...'}</span
>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each enclosure as rec}
<Select.Item value={String(rec.id)}>{rec.name}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
</div>
<div class="space-y-1">
<Label for="enclosure" class="text-xs">Recinto:</Label>
<Select.Root
type="single"
value={formData.enclosure ? String(formData.enclosure) : ''}
onValueChange={(v) => {
formData.enclosure = v ? parseInt(v) : null;
}}
>
<Select.Trigger id="enclosure" class="h-7 text-xs">
<span class="truncate"
>{formData.enclosure
? enclosure.find((p) => p.id === formData.enclosure)?.name || 'Selecciona...'
: 'Selecciona...'}</span
{#if operationType === 1}
<div class="space-y-1 pt-1">
<Label for="valuation_method_exp" class="text-xs font-semibold"
>Método de Valoración:</Label
>
<Select.Root
type="single"
value={formData.valuation_method ? String(formData.valuation_method) : ''}
onValueChange={(v) => {
formData.valuation_method = v;
}}
>
<Select.Trigger id="valuation_method_exp" class="h-8 text-xs">
<span class="truncate">{formData.valuation_method || 'Selecciona...'}</span>
</Select.Trigger>
<Select.Content>
<Select.Item value="general">General</Select.Item>
<Select.Item value="devalued">Devaluado</Select.Item>
<Select.Item value="special">Especial</Select.Item>
</Select.Content>
</Select.Root>
</div>
{/if}
</div>
{/if}
{#if invoiceType === 'MEX'}
<div class="rounded-md border p-3">
<div class="space-y-2">
<Label class="mb-2 block text-xs font-semibold">Es mixto?</Label>
<div class="flex items-center gap-4">
<label class="group flex cursor-pointer items-center gap-2">
<div
class="relative flex h-4 w-4 items-center justify-center rounded-full border-2 border-zinc-400 transition-all {formData.is_mixed ===
true
? 'border-zinc-800 bg-zinc-800'
: ''}"
>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each enclosure as rec}
<Select.Item value={String(rec.id)}>{rec.name}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
<input type="radio" value={true} bind:group={formData.is_mixed} class="sr-only" />
{#if formData.is_mixed === true}
<div class="h-1.5 w-1.5 rounded-full bg-white"></div>{/if}
</div>
<span class="text-xs font-medium transition-colors group-hover:text-zinc-800">Si</span
>
</label>
<label class="group flex cursor-pointer items-center gap-2">
<div
class="relative flex h-4 w-4 items-center justify-center rounded-full border-2 border-zinc-400 transition-all {formData.is_mixed ===
false
? 'border-zinc-800 bg-zinc-800'
: ''}"
>
<input type="radio" value={false} bind:group={formData.is_mixed} class="sr-only" />
{#if formData.is_mixed === false}
<div class="h-1.5 w-1.5 rounded-full bg-white"></div>{/if}
</div>
<span class="text-xs font-medium transition-colors group-hover:text-zinc-800">No</span
>
</label>
</div>
</div>
</div>
{#if operationType === 1}
<div class="space-y-1 pt-1">
<Label for="valuation_method_exp" class="text-xs font-semibold"
>Método de Valoración:</Label
>
<Select.Root
type="single"
value={formData.valuation_method ? String(formData.valuation_method) : ''}
onValueChange={(v) => {
formData.valuation_method = v;
}}
>
<Select.Trigger id="valuation_method_exp" class="h-8 text-xs">
<span class="truncate">{formData.valuation_method || 'Selecciona...'}</span>
</Select.Trigger>
<Select.Content>
<Select.Item value="general">General</Select.Item>
<Select.Item value="devalued">Devaluado</Select.Item>
<Select.Item value="special">Especial</Select.Item>
</Select.Content>
</Select.Root>
</div>
{/if}
</div>
{/if}
</div>
<!-- Extra Bottom Row for Import Mode specific fields (Original Layout preserved for Import) -->
{#if operationType !== 1}
{#if operationType !== 1 && invoiceType !== 'MEX'}
<div class="col-span-2 space-y-3 rounded-md border p-3">
<div class="grid grid-cols-4 gap-3">
<div class="space-y-1">

View File

@@ -4,7 +4,7 @@
import { Label } from '$lib/components/ui/label';
import { Button } from '$lib/components/ui/button';
import { Checkbox } from '$lib/components/ui/checkbox';
import { RadioGroup, RadioGroupItem } from '$lib/components/ui/radio-group';
import * as RadioGroup from '$lib/components/ui/radio-group';
import * as Select from '$lib/components/ui/select';
import { Textarea } from '$lib/components/ui/textarea';
import { Plus, Upload } from 'lucide-svelte';
@@ -15,13 +15,15 @@
formData = $bindable(),
exists = $bindable(),
transportModes = [],
operationType = undefined
operationType = undefined,
invoiceType = undefined
}: {
invoice: Invoice | null;
formData?: any;
exists?: boolean;
transportModes?: any[];
operationType?: number;
invoiceType?: string;
} = $props();
if (!formData && invoice) {
@@ -30,9 +32,11 @@
comments_status: invoice.comments_status || '',
// Campos que van en diferentes recursos pero se editan aquí
transport_mode: invoice.logistics?.transport_mode || null,
is_mixed: invoice.compliance_mx?.is_mixed || null,
is_mixed: invoice.compliance_mx?.is_mixed ? 'si' : 'no',
print_stamp: invoice.financials?.seal_value_2500 || false,
rule_3121_parties_ii: false,
rule_3121_parties_ii: invoice.compliance_mx?.rule_3121_parties_ii || false,
// Errores (Copied from Continuation Tab)
errores_facturacion: [],
related_doc_id: invoice.related_doc_id || null,
code_signature: invoice.compliance_mx?.signature_key || '',
electronic_signature: invoice.compliance_mx?.electronic_signature || '',
@@ -64,6 +68,8 @@
is_mixed: null,
print_stamp: false,
rule_3121_parties_ii: false,
// Errores
errores_facturacion: [],
related_doc_id: null,
code_signature: '',
electronic_signature: '',
@@ -94,231 +100,280 @@
// Función para cargar información
console.log('Cargar información');
}
$effect(() => {
console.log('OthersTabForm props:', {
invoiceType,
operationType,
isMixed: formData.is_mixed,
rule: formData.rule_3121_parties_ii
});
});
</script>
<div class="grid grid-cols-3 grid-rows-1 gap-3">
<div class="space-y-3 rounded-md border p-3">
<!-- Modo de Transporte -->
<div class="space-y-2">
<Label for="transport-mode">Modo de Transporte:</Label>
<Select.Root
type="single"
value={formData.transport_mode}
onValueChange={(value: string | undefined) => (formData.transport_mode = value || 'TRUCK')}
>
<Select.Trigger id="transport-mode">
{transportModes.find((m) => m.key === formData.transport_mode)?.name ||
'Seleccionar modo'}
</Select.Trigger>
<Select.Content>
{#each transportModes as mode}
<Select.Item value={mode.key}>
{mode.name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
{#if operationType !== 1}
<!-- Imprimir Sello -->
{#if invoiceType !== 'MEX'}
<div class="grid grid-cols-3 grid-rows-1 gap-3">
<div class="space-y-3 rounded-md border p-3">
<!-- Modo de Transporte -->
<div class="space-y-2">
<Label class="opacity-0">Spacer</Label>
<div class="flex items-center space-x-2">
<Checkbox id="print-stamp" bind:checked={formData.print_stamp} />
<Label for="print-stamp" class="font-normal">
Imprimir el Sello por Valor menor a 2500 dlls
</Label>
</div>
</div>
<!-- Es Mixto -->
<div class="space-y-2">
<Label class="opacity-0">Spacer</Label>
<Label>Es Mixto?</Label>
<RadioGroup
value={formData.is_mixed === null ? 'no' : formData.is_mixed ? 'yes' : 'no'}
onValueChange={(v) => (formData.is_mixed = v === 'yes')}
class="flex gap-4"
>
<div class="flex items-center space-x-2">
<RadioGroupItem value="yes" id="mixed-yes" />
<Label for="mixed-yes" class="font-normal"></Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroupItem value="no" id="mixed-no" />
<Label for="mixed-no" class="font-normal">No</Label>
</div>
</RadioGroup>
<Label class="opacity-0">Spacer</Label>
</div>
{:else}
<!-- New Export Fields -->
<div class="space-y-2 pt-2">
<Label for="bill_number">Número Master BOL:</Label>
<Input id="bill_number" bind:value={formData.bill_number} class="h-8 text-xs" />
</div>
<div class="space-y-2">
<Label for="guide_number">Número Guía:</Label>
<Input id="guide_number" bind:value={formData.guide_number} class="h-8 text-xs" />
</div>
<div class="space-y-2">
<Label for="shipment_number">Número Embarque:</Label>
<Input id="shipment_number" bind:value={formData.shipment_number} class="h-8 text-xs" />
</div>
<div class="space-y-2 pb-2">
<Label for="option_iv18">Opción IV 18:</Label>
<Label for="transport-mode">Modo de Transporte:</Label>
<Select.Root
type="single"
value={formData.option_iv18}
onValueChange={(v: string | undefined) => (formData.option_iv18 = v || '')}
value={formData.transport_mode}
onValueChange={(value: string | undefined) =>
(formData.transport_mode = value || 'TRUCK')}
>
<Select.Trigger id="option_iv18" class="h-8 text-xs">
{formData.option_iv18 || 'Seleccionar opción'}
<Select.Trigger id="transport-mode">
{transportModes.find((m) => m.key === formData.transport_mode)?.name ||
'Seleccionar modo'}
</Select.Trigger>
<Select.Content>
<Select.Item value="OPCION 1">Opción 1</Select.Item>
<!-- Add more options as needed -->
{#each transportModes as mode}
<Select.Item value={mode.key}>
{mode.name}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<!-- DATOS ENTREGA -->
<div class="mt-4 space-y-3 rounded-md border bg-muted/20 p-3">
<h4 class="mb-2 border-b pb-1 text-xs font-bold text-muted-foreground uppercase">
Datos Entrega
</h4>
<div class="mb-2 flex items-center space-x-2">
<Checkbox id="delivered_status" bind:checked={formData.delivered_status} />
<Label for="delivered_status" class="text-xs font-semibold">Entregado</Label>
</div>
{#if operationType !== 1 && invoiceType !== 'MEX'}
<!-- Imprimir Sello -->
<div class="space-y-2">
<Label for="received_by" class="text-xs">Recibido por:</Label>
<Input
id="received_by"
bind:value={formData.received_by}
disabled={!formData.delivered_status}
class="h-7 text-xs"
/>
</div>
<div class="space-y-2">
<Label for="delivery_date" class="text-xs">Fecha Entrega:</Label>
<Input
id="delivery_date"
type="date"
bind:value={formData.delivery_date}
disabled={!formData.delivered_status}
class="h-7 text-xs"
/>
</div>
</div>
{/if}
{#if operationType !== 1}
<div class="flex items-center space-x-2">
<Checkbox id="rule-3121" bind:checked={formData.rule_3121_parties_ii} />
<Label for="rule-3121" class="font-normal">Regla 3.1.21 Partes II</Label>
</div>
{/if}
<!-- Comentario estatus -->
<div class="space-y-2">
<Label class="opacity-0">Spacer</Label>
<Label>Comentario Estatus:</Label>
<Textarea
id="comments_status"
bind:value={formData.comments_status}
placeholder="Comentario estatus"
rows={3}
/>
</div>
</div>
<div class="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">
<Label for="relation-docs-id">ID Relación Docs:</Label>
<Input id="relation-docs-id" type="number" bind:value={formData.related_doc_id} />
</div>
<!-- Firma Electrónica -->
<div class="space-y-2 md:col-span-2">
<Label for="electronic-sig-1">Firma Electrónica:</Label>
<div class="flex gap-2">
<Input id="electronic-sig-1" bind:value={formData.code_signature} class="flex-1" />
<Button variant="outline" size="icon">
<Upload class="h-4 w-4" />
</Button>
</div>
</div>
<!-- Mandatario/Persona Autorizada -->
<div class="space-y-2">
<Label for="mandatory-person">Mandatario/Persona Autorizada:</Label>
<Input id="mandatory-person" bind:value={formData.mandatory_person} />
</div>
<!-- RFC -->
<div class="space-y-2">
<Label id="rfc" for="rfc">RFC: {rfc}</Label>
</div>
<!-- CURP -->
<div class="space-y-2">
<Label id="curp" for="curp">CURP: {curp}</Label>
</div>
<!-- Modo Contingencia -->
<div class="col-span-4 space-y-2">
<div class="flex items-center space-x-2">
<Checkbox id="contingency-mode" bind:checked={formData.contingency_mode} />
<Label for="contingency-mode" class="font-normal">Modo Contingencia</Label>
</div>
<!-- COVE -->
<div class="space-y-2">
<Label for="cove">COVE:</Label>
<Input id="cove" bind:value={formData.cove} placeholder="COVE" />
</div>
<!-- Número de Operación -->
<div class="space-y-2">
<Label for="operation-num">Núm Operación:</Label>
<Input id="operation-num" bind:value={formData.operation_num} />
</div>
<!-- Adenda(s) -->
<div class="space-y-2 md:col-span-2">
<Label for="adendas">Adenda(s):</Label>
<Input id="adendas" bind:value={formData.adendas} />
</div>
<!-- Observaciones VU -->
<div class="space-y-2 md:col-span-2">
<Label for="observations-vu">Observaciones VU:</Label>
<div class="flex gap-2">
<Textarea
id="observations-vu"
bind:value={formData.observations_vu}
class="min-h-[60px] flex-1"
/>
<Button variant="outline" onclick={loadInfo}>Cargar Info.</Button>
<Label class="opacity-0">Spacer</Label>
<div class="flex items-center space-x-2">
<Checkbox id="print-stamp" bind:checked={formData.print_stamp} />
<Label for="print-stamp" class="text-xs font-normal">
Imprimir el Sello por Valor menor a 2500 dlls
</Label>
</div>
</div>
<!-- Número Certificado -->
{#if invoiceType !== 'CR' && invoiceType !== 'REP' && invoiceType !== 'REPAR'}
<!-- Es Mixto -->
<div class="space-y-2">
<Label class="opacity-0">Spacer</Label>
<Label class="text-xs">Es Mixto?</Label>
<RadioGroup.Root bind:value={formData.is_mixed} class="flex gap-4">
<div class="flex items-center space-x-2">
<RadioGroup.Item value="si" id="mixed-si" class="h-4 w-4" />
<Label for="mixed-si" class="cursor-pointer text-xs font-normal"></Label>
</div>
<div class="flex items-center space-x-2">
<RadioGroup.Item value="no" id="mixed-no" class="h-4 w-4" />
<Label for="mixed-no" class="cursor-pointer text-xs font-normal">No</Label>
</div>
</RadioGroup.Root>
</div>
{/if}
{/if}
{#if (operationType === 1 || invoiceType === 'CR') && invoiceType !== 'REP' && invoiceType !== 'REPAR'}
<!-- Export Fields (Hidden for Repair) -->
<div class="space-y-2 pt-2">
<Label for="bill_number">Número Master BOL:</Label>
<Input id="bill_number" bind:value={formData.bill_number} class="h-8 text-xs" />
</div>
<div class="space-y-2">
<Label for="certified-num">Número Certificado:</Label>
<Input id="certified-num" bind:value={formData.certified_number} />
<Label for="guide_number">Número Guía:</Label>
<Input id="guide_number" bind:value={formData.guide_number} class="h-8 text-xs" />
</div>
<div class="space-y-2">
<Label for="shipment_number">Número Embarque:</Label>
<Input id="shipment_number" bind:value={formData.shipment_number} class="h-8 text-xs" />
</div>
<div class="space-y-2 pb-2">
<Label for="option_iv18">Opción IV 18:</Label>
<Select.Root
type="single"
value={formData.option_iv18}
onValueChange={(v: string | undefined) => (formData.option_iv18 = v || '')}
>
<Select.Trigger id="option_iv18" class="h-8 text-xs">
{formData.option_iv18 || 'Seleccionar opción'}
</Select.Trigger>
<Select.Content>
<Select.Item value="OPCION 1">Opción 1</Select.Item>
</Select.Content>
</Select.Root>
</div>
<!-- Firma Electrónica 2 -->
<!-- DATOS ENTREGA -->
<div class="mt-4 space-y-3 rounded-md border bg-muted/20 p-3">
<h4 class="mb-2 border-b pb-1 text-xs font-bold text-muted-foreground uppercase">
Datos Entrega
</h4>
<div class="mb-2 flex items-center space-x-2">
<Checkbox id="delivered_status" bind:checked={formData.delivered_status} />
<Label for="delivered_status" class="text-xs font-semibold">Entregado</Label>
</div>
<div class="space-y-2">
<Label for="received_by" class="text-xs">Recibido por:</Label>
<Input
id="received_by"
bind:value={formData.received_by}
disabled={!formData.delivered_status}
class="h-7 text-xs"
/>
</div>
<div class="space-y-2">
<Label for="delivery_date" class="text-xs">Fecha Entrega:</Label>
<Input
id="delivery_date"
type="date"
bind:value={formData.delivery_date}
disabled={!formData.delivered_status}
class="h-7 text-xs"
/>
</div>
</div>
{/if}
{#if operationType !== 1 && invoiceType !== 'CR' && invoiceType !== 'REP' && invoiceType !== 'REPAR'}
<div class="flex items-center space-x-2 py-2">
<Checkbox id="rule-3121" bind:checked={formData.rule_3121_parties_ii} />
<Label for="rule-3121" class="text-xs font-normal">Regla 3.1.21 Partes II</Label>
</div>
{/if}
<!-- Comentario estatus -->
<div class="space-y-2">
<Label class="opacity-0">Spacer</Label>
<Label>Comentario Estatus:</Label>
<Textarea
id="comments_status"
bind:value={formData.comments_status}
placeholder="Comentario estatus"
rows={3}
/>
</div>
</div>
<div class="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">
<Label for="relation-docs-id">ID Relación Docs:</Label>
<Input id="relation-docs-id" type="number" bind:value={formData.related_doc_id} />
</div>
<!-- Firma Electrónica -->
<div class="space-y-2 md:col-span-2">
<Label for="electronic-sig-2">Firma Electrónica:</Label>
<Input id="electronic-sig-2" bind:value={formData.electronic_signature} />
<Label for="electronic-sig-1">Firma Electrónica:</Label>
<div class="flex gap-2">
<Input id="electronic-sig-1" bind:value={formData.code_signature} class="flex-1" />
<Button variant="outline" size="icon">
<Upload class="h-4 w-4" />
</Button>
</div>
</div>
<!-- Mandatario/Persona Autorizada -->
<div class="space-y-2">
<Label for="mandatory-person">Mandatario/Persona Autorizada:</Label>
<Input id="mandatory-person" bind:value={formData.mandatory_person} />
</div>
<!-- RFC -->
<div class="space-y-2">
<Label id="rfc" for="rfc">RFC: {rfc}</Label>
</div>
<!-- CURP -->
<div class="space-y-2">
<Label id="curp" for="curp">CURP: {curp}</Label>
</div>
<!-- Modo Contingencia -->
<div class="col-span-4 space-y-2">
<div class="flex items-center space-x-2">
<Checkbox id="contingency-mode" bind:checked={formData.contingency_mode} />
<Label for="contingency-mode" class="font-normal">Modo Contingencia</Label>
</div>
<!-- COVE -->
<div class="space-y-2">
<Label for="cove">COVE:</Label>
<Input id="cove" bind:value={formData.cove} placeholder="COVE" />
</div>
<!-- Número de Operación -->
<div class="space-y-2">
<Label for="operation-num">Núm Operación:</Label>
<Input id="operation-num" bind:value={formData.operation_num} />
</div>
<!-- Adenda(s) -->
<div class="space-y-2 md:col-span-2">
<Label for="adendas">Adenda(s):</Label>
<Input id="adendas" bind:value={formData.adendas} />
</div>
<!-- Observaciones VU -->
<div class="space-y-2 md:col-span-2">
<Label for="observations-vu">Observaciones VU:</Label>
<div class="flex gap-2">
<Textarea
id="observations-vu"
bind:value={formData.observations_vu}
class="min-h-[60px] flex-1"
/>
<Button variant="outline" onclick={loadInfo}>Cargar Info.</Button>
</div>
</div>
<!-- Número Certificado -->
<div class="space-y-2">
<Label for="certified-num">Número Certificado:</Label>
<Input id="certified-num" bind:value={formData.certified_number} />
</div>
<!-- Firma Electrónica 2 -->
<div class="space-y-2 md:col-span-2">
<Label for="electronic-sig-2">Firma Electrónica:</Label>
<Input id="electronic-sig-2" bind:value={formData.electronic_signature} />
</div>
</div>
</div>
</div>
</div>
</div>
{:else}
<div class="space-y-4 rounded-md border p-4">
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Errores de Facturación</h4>
<div class="rounded border">
<table class="w-full text-xs">
<thead class="bg-muted">
<tr>
<th class="border px-2 py-1 text-left">Línea</th>
<th class="border px-2 py-1 text-left">Clave</th>
<th class="border px-2 py-1 text-left">Descripción</th>
</tr>
</thead>
<tbody>
{#if formData.errores_facturacion?.length}
{#each formData.errores_facturacion as error}
<tr>
<td class="border px-2 py-1">{error.linea}</td>
<td class="border px-2 py-1">{error.clave}</td>
<td class="border px-2 py-1">{error.descripcion}</td>
</tr>
{/each}
{:else}
<tr>
<td colspan="3" class="border px-2 py-12 text-center text-muted-foreground">
Sin errores registrados
</td>
</tr>
{/if}
</tbody>
</table>
</div>
<div class="flex justify-end gap-2">
<Button variant="outline" size="sm" class="h-7 text-xs">Insertar</Button>
<Button variant="outline" size="sm" class="h-7 text-xs">Editar</Button>
<Button variant="outline" size="sm" class="h-7 text-xs">Borrar</Button>
</div>
</div>
{/if}

View File

@@ -57,7 +57,7 @@ export async function saveInvoice(options: SaveInvoiceOptions): Promise<SaveInvo
missingFields.push(requiredFields.invoice_number);
}
// Validar campos de generalFormData
if (!generalFormData?.document_type) {
if (!generalFormData?.document_type && InvoiceTopFieldsFormData?.invoice_type !== 'MEX') {
missingFields.push(requiredFields.document_type);
}
@@ -118,9 +118,9 @@ function buildInvoicePayload(formData: FormDataSet): CreateInvoiceData | UpdateI
emission_date: InvoiceTopFieldsFormData?.emission_date || undefined,
proforma_number: observationFormData?.proforma_number || undefined,
// Observation fields from observationFormData
observation_es: observationFormData?.observation_es || undefined,
observation_en: observationFormData?.observation_en || undefined,
alternate_invoice: observationFormData?.alternate_invoice || undefined,
observation_es: observationFormData ? observationFormData.observation_es : undefined,
observation_en: observationFormData ? observationFormData.observation_en : undefined,
alternate_invoice: InvoiceTopFieldsFormData?.alternate_invoice || (observationFormData ? observationFormData.alternate_invoice : undefined),
// Fields from othersFormData and continuationFormData that go to invoice header
related_doc_id: othersFormData?.related_doc_id || undefined,
vu_observations: othersFormData?.observations_vu || undefined,
@@ -151,12 +151,13 @@ function buildInvoicePayload(formData: FormDataSet): CreateInvoiceData | UpdateI
othersFormData?.code_signature || othersFormData?.electronic_signature ||
othersFormData?.mandatory_person ||
continuationFormData?.puerto_entrada || continuationFormData?.reason_export ||
continuationFormData?.is_mixed ||
generalFormData?.manifest_number ||
generalFormData?.shipped_by_id || generalFormData?.shipped_by_header;
continuationFormData?.is_mixed !== undefined ||
observationFormData?.is_mixed !== undefined || observationFormData?.sub_division !== undefined || observationFormData?.office_document !== undefined ||
generalFormData?.manifest_number !== undefined ||
generalFormData?.shipped_by_id !== undefined || generalFormData?.shipped_by_header !== undefined;
if (hasComplianceValue) {
payload.compliance_mx = buildComplianceMxData(InvoiceTopFieldsFormData, generalFormData, othersFormData, observationFormData, continuationFormData);
payload.compliance_mx = buildComplianceMxData(InvoiceTopFieldsFormData?.invoice_type, InvoiceTopFieldsFormData, generalFormData, othersFormData, observationFormData, continuationFormData);
}
// Financials
@@ -168,7 +169,7 @@ function buildInvoicePayload(formData: FormDataSet): CreateInvoiceData | UpdateI
observationFormData?.total_increments_me || othersFormData?.print_stamp;
if (hasFinancialsValue) {
payload.financials = buildFinancialsData(generalFormData, observationFormData, othersFormData);
payload.financials = buildFinancialsData(generalFormData, observationFormData, othersFormData, InvoiceTopFieldsFormData);
}
// Logistics
@@ -184,7 +185,7 @@ function buildInvoicePayload(formData: FormDataSet): CreateInvoiceData | UpdateI
return payload;
}
function buildComplianceMxData(InvoiceTopFieldsFormData: any, generalFormData: any, othersFormData: any, observationFormData: any, continuationFormData: any) {
function buildComplianceMxData(invoiceType: string, InvoiceTopFieldsFormData: any, generalFormData: any, othersFormData: any, observationFormData: any, continuationFormData: any) {
return {
// Pedimento fields - desde InvoiceTopFieldsFormData
pedimento_id: InvoiceTopFieldsFormData?.pedimento_id ? Number(InvoiceTopFieldsFormData.pedimento_id) : null,
@@ -207,8 +208,13 @@ function buildComplianceMxData(InvoiceTopFieldsFormData: any, generalFormData: a
// Fields from observationFormData
movement_type: observationFormData?.movement_type || null,
enclosure: observationFormData?.enclosure || null,
// Fields from othersFormData or continuationFormData (priority to continuation in export)
is_mixed: continuationFormData?.is_mixed === 'si' ? true : (othersFormData?.is_mixed !== undefined ? othersFormData.is_mixed : null),
// Fields from othersFormData or continuationFormData (priority based on invoice type)
is_mixed: invoiceType === 'MEX'
? (observationFormData?.is_mixed || false)
: (invoiceType === 'CR' || invoiceType === 'REP' || InvoiceTopFieldsFormData?.operation_type === 'exp'
? (continuationFormData?.is_mixed === 'si')
: (othersFormData?.is_mixed === 'si')),
is_regime_change: invoiceType === 'CR' ? true : (othersFormData?.contingency_mode !== undefined ? othersFormData.contingency_mode : false), // Note: reuse check logic if exists elsewhere, but here we explicitly set it for CR
reason_export: continuationFormData?.reason_export || null,
acts_as: othersFormData?.mandatory_person || null,
contingency_mode: othersFormData?.contingency_mode || false,
@@ -217,19 +223,20 @@ function buildComplianceMxData(InvoiceTopFieldsFormData: any, generalFormData: a
vucem_operation_num: othersFormData?.operation_num || null,
addendum_vu: othersFormData?.adendas || null,
certificate_number: othersFormData?.certified_number || null,
signature_key: othersFormData?.code_signature || null,
electronic_signature: othersFormData?.electronic_signature || null,
signature_key: othersFormData?.code_signature || generalFormData?.code_signature || null,
electronic_signature: othersFormData?.electronic_signature || generalFormData?.electronic_signature || null,
subdivision: observationFormData?.sub_division || null,
office_document: observationFormData?.office_document || null,
rule_3121_parties_ii: othersFormData?.rule_3121_parties_ii || false,
};
}
function buildFinancialsData(generalFormData: any, observationFormData: any, othersFormData: any) {
function buildFinancialsData(generalFormData: any, observationFormData: any, othersFormData: any, InvoiceTopFieldsFormData?: any) {
return {
// Currency from generalFormData
currency_type: generalFormData?.currency_type || null,
currency: generalFormData?.currency || null,
iva_factor: generalFormData?.iva_factor !== null && generalFormData?.iva_factor !== undefined ? String(generalFormData.iva_factor) : null,
iva_factor: (InvoiceTopFieldsFormData?.iva_factor || generalFormData?.iva_factor) ? String(InvoiceTopFieldsFormData?.iva_factor || generalFormData.iva_factor) : null,
// Costs & increments from observationFormData
freight: observationFormData?.freight || null,
insurance: observationFormData?.insurance || null,
@@ -278,5 +285,6 @@ function buildLogisticsData(generalFormData: any, observationFormData: any, othe
green_light_us: continuationFormData?.semaforo_verde_aduana_americana || false,
red_light_mx: continuationFormData?.semaforo_rojo_aduana_mexicana || false,
red_light_us: continuationFormData?.semaforo_rojo_aduana_americana || false,
vehicle_data: continuationFormData?.vehicle_data || null,
};
}

View File

@@ -355,6 +355,10 @@ export function getSidebarData(): SidebarData {
{
title: m["sidebar.import_invoices.regime_change"](),
url: "/dashboard/invoices?operation_type=imp&invoice_type=CR",
},
{
title: m["sidebar.import_invoices.repair"](),
url: "/dashboard/invoices?operation_type=imp&invoice_type=REP",
}
],
},

View File

@@ -17,7 +17,15 @@
import type { PageData } from './$types';
import { browser } from '$app/environment';
import { companyStore } from '$lib/stores/company.svelte';
import { Plus, RefreshCw, FileText, RotateCcw, Boxes, Package, ClipboardList } from 'lucide-svelte';
import {
Plus,
RefreshCw,
FileText,
RotateCcw,
Boxes,
Package,
ClipboardList
} from 'lucide-svelte';
// IMPORTANTE: Asegúrate de tener instalada svelte-sonner para las notificaciones
import { toast } from 'svelte-sonner';
@@ -39,7 +47,7 @@
year: data.filters?.year || ''
});
let isDownloadModalOpen = $state(false);
let isDownloadModalOpen = $state(false);
// Efecto reactivo para actualizar filtros cuando cambian los query parameters en la URL
$effect(() => {
@@ -611,32 +619,36 @@
currentStatusFunction = null;
}
// --- AQUÍ PASAMOS LA FUNCIÓN DE DESCARGA A LAS COLUMNAS ---
const columns = createColumns(handleSuccess);
async function handleModalConfirm(type: string, format: string, currency: string, uomSource: string, weightUnit: string) {
if (!selectedInvoice || !companyStore.activeCompany) return;
// --- AQUÍ PASAMOS LA FUNCIÓN DE DESCARGA A LAS COLUMNAS ---
const columns = createColumns(handleSuccess);
async function handleModalConfirm(
type: string,
format: string,
currency: string,
uomSource: string,
weightUnit: string
) {
if (!selectedInvoice || !companyStore.activeCompany) return;
try {
// 1. Trigger: Start celery task with selected options
// Note: uomSource and weightUnit are UI-only for now
const { task_id } = await invoicesReportsApi.triggerPdfGeneration(
selectedInvoice.id,
companyStore.activeCompany.id,
type,
currency
);
// 2. Open Progress Dialog
currentTaskId = task_id;
currentStatusFunction = invoicesReportsApi.getTaskStatus;
showProgressDialog = true;
} catch (error) {
console.error(error);
toast.error("No se pudo iniciar la descarga");
}
}
try {
// 1. Trigger: Start celery task with selected options
// Note: uomSource and weightUnit are UI-only for now
const { task_id } = await invoicesReportsApi.triggerPdfGeneration(
selectedInvoice.id,
companyStore.activeCompany.id,
type,
currency
);
// 2. Open Progress Dialog
currentTaskId = task_id;
currentStatusFunction = invoicesReportsApi.getTaskStatus;
showProgressDialog = true;
} catch (error) {
console.error(error);
toast.error('No se pudo iniciar la descarga');
}
}
</script>
<div class="space-y-6">
@@ -659,13 +671,13 @@
>
</Card.Header>
<Card.Content>
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-5">
<div class="space-y-2">
<Label for="filter-operation-type">Tipo de Operación</Label>
<select
id="filter-operation-type"
bind:value={filters.operation_type}
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none"
>
{#each operationTypeOptions as option}
<option value={option.value}>{option.label}</option>
@@ -678,7 +690,7 @@
<select
id="filter-invoice-type"
bind:value={filters.invoice_type}
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none"
>
{#each invoiceTypeOptions() as option}
<option value={option.value}>{option.label}</option>
@@ -759,82 +771,77 @@
<!-- Footer fijo con botones de acción -->
<div
class="fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t shadow-lg z-[5] group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] ml-[calc(var(--sidebar-width))]"
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"
>
<div class="px-4 py-4 max-w-[1400px] mx-auto">
<div class="mx-auto max-w-[1400px] px-4 py-4">
<!-- Botones de acción -->
<div class="flex justify-end gap-2">
<Button variant="outline" size="sm" disabled={!selectedInvoice}>
<RefreshCw class="h-4 w-4 mr-2" />
<RefreshCw class="mr-2 h-4 w-4" />
Actualizar
</Button>
<Button variant="outline" size="sm" disabled={!selectedInvoice}>
<RotateCcw class="h-4 w-4 mr-2" />
<RotateCcw class="mr-2 h-4 w-4" />
Desactualizar
</Button>
<Button
variant="outline"
size="sm"
onclick={() => isDownloadModalOpen = true}
onclick={() => (isDownloadModalOpen = true)}
disabled={!selectedInvoice}
>
<FileText class="h-4 w-4 mr-2" />
<FileText class="mr-2 h-4 w-4" />
Factura
</Button>
<Button
variant="outline"
size="sm"
onclick={() => selectedInvoice && handleDownloadConsolidated(selectedInvoice)}
disabled={!selectedInvoice}
>
<Boxes class="h-4 w-4 mr-2" />
<Boxes class="mr-2 h-4 w-4" />
Consolidado
</Button>
<Button
variant="outline"
size="sm"
onclick={() => selectedInvoice && handleDownloadAvisoConsolidado(selectedInvoice)}
disabled={!selectedInvoice}
>
<Boxes class="mr-2 h-4 w-4" />
Aviso Consolidado
</Button>
<Button
variant="outline"
size="sm"
onclick={() => selectedInvoice && handleDownloadPackingList(selectedInvoice)}
disabled={!selectedInvoice}
>
<Package class="mr-2 h-4 w-4" />
Packing List
</Button>
{#if selectedInvoice?.operation_type === 'exp'}
<Button
variant="outline"
size="sm"
onclick={() => selectedInvoice && handleDownloadDescargo(selectedInvoice)}
onclick={() => selectedInvoice && handleDownloadAvisoConsolidado(selectedInvoice)}
disabled={!selectedInvoice}
>
<ClipboardList class="mr-2 h-4 w-4" />
Descargo PEPS
<Boxes class="mr-2 h-4 w-4" />
Aviso Consolidado
</Button>
{/if}
<Button
variant="outline"
size="sm"
onclick={() => selectedInvoice && handleDownloadPackingList(selectedInvoice)}
disabled={!selectedInvoice}
>
<Package class="mr-2 h-4 w-4" />
Packing List
</Button>
{#if selectedInvoice?.operation_type === 'exp'}
<Button
variant="outline"
size="sm"
onclick={() => selectedInvoice && handleDownloadDescargo(selectedInvoice)}
disabled={!selectedInvoice}
>
<ClipboardList class="mr-2 h-4 w-4" />
Descargo PEPS
</Button>
{/if}
</div>
</div>
</div>
<!-- ... -->
<!-- ... -->
{#if selectedInvoice && companyStore.activeCompany}
<InvoiceDownloadModal
bind:open={isDownloadModalOpen}
onConfirm={handleModalConfirm}
/>
{/if}
{#if selectedInvoice && companyStore.activeCompany}
<InvoiceDownloadModal bind:open={isDownloadModalOpen} onConfirm={handleModalConfirm} />
{/if}
</div>

View File

@@ -145,9 +145,9 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
if (params.id === 'new') {
try {
const [
invoiceTypesResponse,
customsBrokersResponse,
clientsResponse,
invoiceTypesResponse,
customsBrokersResponse,
clientsResponse,
providersResponse,
currencyTypesResponse,
transportTypesResponse,
@@ -191,7 +191,7 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
const drivers = driversResponse.ok ? await driversResponse.json() : { items: [] };
const trailers = trailersResponse.ok ? await trailersResponse.json() : { items: [] };
const customsSections = customsSectionsResponse.ok ? await customsSectionsResponse.json() : { items: [] };
const codePedimentoRegimens = codePedimentoRegimensResponse.ok ? await codePedimentoRegimensResponse.json() : { items: [] };
const codePedimentoRegimens = codePedimentoRegimensResponse.ok ? await codePedimentoRegimensResponse.json() : { items: [] };
const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] };
const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] };
const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] };
@@ -276,9 +276,9 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
// Cargar también los datos de referencia para edición
const [
invoiceTypesResponse,
customsBrokersResponse,
clientsResponse,
invoiceTypesResponse,
customsBrokersResponse,
clientsResponse,
providersResponse,
currencyTypesResponse,
transportTypesResponse,

View File

@@ -112,12 +112,22 @@
let showExchangeRateDialog = $state(false);
let missingExchangeRateDate = $state('');
// Derivados reactivos para el tipo de operación
// Derivados reactivos para el tipo de factura y operación
let invoiceType = $derived.by(() => {
return (
InvoiceTopFieldsFormData?.invoice_type ||
data.filters?.invoice_type ||
data.invoice?.invoice_type ||
'TEM'
); // Default to TEM if not found
});
let operationTypeText = $derived.by(() => {
const type =
InvoiceTopFieldsFormData?.operation_type ||
data.invoice?.operation_type ||
data.filters?.operation_type;
if (invoiceType === 'CR') return 'CAMBIO DE RÉGIMEN';
if (type === 'exp') return 'EXPORTACIÓN';
if (type === 'imp') return 'IMPORTACIÓN';
return 'TIPO NO DEFINIDO';
@@ -390,6 +400,7 @@
pedimentos={data.pedimentos || []}
defaultOperationType={data.filters?.operation_type ?? undefined}
defaultInvoiceType={data.filters?.invoice_type ?? undefined}
{invoiceType}
/>
<Tabs.Content value="general">
@@ -397,6 +408,7 @@
invoice={data.invoice}
bind:formData={generalFormData}
invoiceTypes={data.invoiceTypes || []}
{invoiceType}
customsBrokers={data.customsBrokers || []}
clients={data.clients || []}
providers={data.providers || []}
@@ -431,6 +443,7 @@
seals={data.seals || []}
incoterms={data.incoterms || []}
enclosure={data.enclosure || []}
{invoiceType}
operationType={InvoiceTopFieldsFormData?.operation_type === 'exp'
? 1
: InvoiceTopFieldsFormData?.operation_type === 'imp'
@@ -448,6 +461,7 @@
invoice={data.invoice}
bind:formData={itemsFormData}
bind:exists={itemsExists}
{invoiceType}
operationType={InvoiceTopFieldsFormData?.operation_type === 'exp'
? 1
: InvoiceTopFieldsFormData?.operation_type === 'imp'
@@ -465,6 +479,7 @@
invoice={data.invoice}
bind:formData={othersFormData}
bind:exists={othersExists}
{invoiceType}
operationType={InvoiceTopFieldsFormData?.operation_type === 'exp'
? 1
: InvoiceTopFieldsFormData?.operation_type === 'imp'
@@ -478,22 +493,25 @@
/>
</Tabs.Content>
<Tabs.Content value="continuation">
<ContinuationTabForm
invoice={data.invoice}
bind:formData={continuationFormData}
bind:exists={continuationExists}
operationType={InvoiceTopFieldsFormData?.operation_type === 'exp'
? 1
: InvoiceTopFieldsFormData?.operation_type === 'imp'
? 2
: data.invoice?.operation_type === 'exp'
? 1
: data.invoice?.operation_type === 'imp'
? 2
: undefined}
/>
</Tabs.Content>
{#if invoiceType !== 'MEX'}
<Tabs.Content value="continuation">
<ContinuationTabForm
invoice={data.invoice}
bind:formData={continuationFormData}
bind:exists={continuationExists}
{invoiceType}
operationType={InvoiceTopFieldsFormData?.operation_type === 'exp'
? 1
: InvoiceTopFieldsFormData?.operation_type === 'imp'
? 2
: data.invoice?.operation_type === 'exp'
? 1
: data.invoice?.operation_type === 'imp'
? 2
: undefined}
/>
</Tabs.Content>
{/if}
</Tabs.Root>
</div>
</div>
@@ -521,7 +539,11 @@
<!-- Tabs Navigation -->
<Tabs.Root bind:value={activeTab}>
<div class="w-full overflow-x-auto pb-2">
<Tabs.List class="inline-flex md:grid md:w-full md:grid-cols-5">
<Tabs.List
class="inline-flex md:grid md:w-full {invoiceType === 'MEX'
? 'md:grid-cols-4'
: 'md:grid-cols-5'}"
>
<Tabs.Trigger value="general" disabled={false} class="whitespace-nowrap">
<FileText size={16} class="mr-2" />
General
@@ -538,10 +560,12 @@
<Truck size={16} class="mr-2" />
Otros
</Tabs.Trigger>
<Tabs.Trigger value="continuation" disabled={false} class="whitespace-nowrap">
<Package size={16} class="mr-2" />
Cont.
</Tabs.Trigger>
{#if invoiceType !== 'MEX'}
<Tabs.Trigger value="continuation" disabled={false} class="whitespace-nowrap">
<Package size={16} class="mr-2" />
Cont.
</Tabs.Trigger>
{/if}
</Tabs.List>
</div>
</Tabs.Root>