Merge pull request 'fix(validators): add required validations for customs fields in item creation and update' (#67) from fix/notificacion-campos into fix/quitar-obligatorio-partes

Reviewed-on: ADUANASOFT/anexo76#67
This commit is contained in:
2026-01-22 23:24:38 +00:00
6 changed files with 129 additions and 21 deletions

View File

@@ -109,4 +109,22 @@ def validate_create(
message="Description in Spanish es obligatorio",
solution="Proporciona una descripción del item en español",
code="REQUIRED"
)
# 8. Validar customs.origin_country
if not line.customs or not line.customs.origin_country:
errors.add_error(
field="customs.origin_country",
message="País de Origen es obligatorio",
solution="Selecciona el país de origen del item",
code="REQUIRED"
)
# 9. Validar customs.fraction_type
if not line.customs or not line.customs.fraction_type:
errors.add_error(
field="customs.fraction_type",
message="Tipo de Tarifa es obligatorio",
solution="Selecciona el tipo de tarifa (GENERAL, PROSEC, ALADI, TLCS)",
code="REQUIRED"
)

View File

@@ -138,14 +138,25 @@ def validate_update(
# 8. Validar datos aduanales si se proporcionan
if line.customs:
# Validar país de origen
if line.customs.origin_country is not None and not line.customs.origin_country:
errors.add_error(
field="customs.origin_country",
message="Origin Country no puede estar vacío",
solution="Selecciona el país de origen del item",
code="REQUIRED"
)
# Validar país de origen (OBLIGATORIO)
if line.customs.origin_country is not None:
if not line.customs.origin_country:
errors.add_error(
field="customs.origin_country",
message="País de Origen es obligatorio",
solution="Selecciona el país de origen del item",
code="REQUIRED"
)
# Validar tipo de tarifa (OBLIGATORIO)
if line.customs.fraction_type is not None:
if not line.customs.fraction_type:
errors.add_error(
field="customs.fraction_type",
message="Tipo de Tarifa es obligatorio",
solution="Selecciona el tipo de tarifa (GENERAL, PROSEC, ALADI, TLCS)",
code="REQUIRED"
)
# Validar preferencia arancelaria
if line.customs.preference is not None and not line.customs.preference:
@@ -184,15 +195,16 @@ def validate_update(
code="INVALID_VALUE"
)
# 9. Validar descripción en español si se proporciona
# 9. Validar descripción en español (OBLIGATORIA)
if line.description and hasattr(line.description, 'description_spanish'):
if line.description.description_spanish is not None and not line.description.description_spanish:
errors.add_error(
field="description.description_spanish",
message="La descripción en español no puede estar vacía",
solution="Proporciona una descripción del item en español",
code="REQUIRED"
)
if line.description.description_spanish is not None:
if not line.description.description_spanish.strip():
errors.add_error(
field="description.description_spanish",
message="Descripción en Español es obligatoria",
solution="Proporciona una descripción del item en español",
code="REQUIRED"
)
# 10. Validar subpartidas si se actualizan
if line.fa_data and line.fa_data.is_subitem:

View File

@@ -23,6 +23,7 @@
editingItem = $bindable(),
invoice,
onSave,
onCancel,
isSaving = false
}: {
open: boolean;
@@ -30,6 +31,7 @@
editingItem: Partial<Item>;
invoice: Invoice | null;
onSave: () => void;
onCancel?: () => void;
isSaving?: boolean;
} = $props();
@@ -55,7 +57,7 @@
</p>
</div>
</div>
<Button variant="ghost" size="icon" onclick={() => open = false} class="h-7 w-7 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800">
<Button variant="ghost" size="icon" onclick={() => onCancel?.()} class="h-7 w-7 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800">
<X class="w-3.5 h-3.5" />
</Button>
</div>
@@ -164,7 +166,7 @@
<footer class="bg-white dark:bg-zinc-950 border-t border-zinc-200 dark:border-zinc-800 px-3 py-1.5 shadow-sm shrink-0">
<div class="flex items-center justify-end gap-1.5">
<Button variant="outline" size="sm" onclick={() => open = false} disabled={isSaving} class="h-7 text-xs px-2">
<Button variant="outline" size="sm" onclick={() => onCancel?.()} disabled={isSaving} class="h-7 text-xs px-2">
Cancelar
</Button>
<Button size="sm" onclick={onSave} disabled={isSaving} class="h-7 text-xs px-2 bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white">

View File

@@ -252,6 +252,7 @@
<div class="space-y-1">
<Label for="tipo_tarifa" class="text-xs font-medium">Tariff Type: <span class="text-red-500">*</span></Label>
<select id="tipo_tarifa" bind:value={customs.fraction_type} class="flex h-8 w-auto min-w-[140px] rounded-md border border-input bg-background px-2 py-1 text-xs ring-offset-background">
<option value={undefined}>Selecciona...</option>
<option value="GENERAL">GENERAL</option>
<option value="PROSEC">PROSEC</option>
<option value="ALADI">ALADI</option>

View File

@@ -14,6 +14,7 @@
editingItem = $bindable(),
invoice,
onSave,
onCancel,
isSaving = false
}: {
open: boolean;
@@ -21,6 +22,7 @@
editingItem: Partial<Item>;
invoice: Invoice | null;
onSave: () => void;
onCancel?: () => void;
isSaving?: boolean;
} = $props();
</script>
@@ -258,7 +260,7 @@
</Tabs.Root>
<Sheet.Footer class="mt-6 gap-2">
<Button variant="outline" onclick={() => open = false} disabled={isSaving}>
<Button variant="outline" onclick={() => onCancel?.()} disabled={isSaving}>
Cancelar
</Button>
<Button onclick={onSave} disabled={isSaving}>

View File

@@ -53,6 +53,7 @@
let isEditMode = $state(false);
let showDeleteDialog = $state(false);
let selectedItem = $state<Item | null>(null);
let originalItemData = $state<Partial<Item> | null>(null); // Guardar estado original para cancelar
let editingItem = $state<Partial<Item>>({
invoice_id: undefined,
reference_number: '',
@@ -179,7 +180,7 @@
},
customs: {
fraction: undefined,
fraction_type: 'GENERAL',
fraction_type: undefined,
american_fraction: undefined,
origin_country: undefined,
destination_country: undefined,
@@ -211,7 +212,9 @@
isEditMode = true;
selectedItem = lineData.full_item;
// Deep clone and normalize numeric values
editingItem = normalizeItemData({ ...lineData.full_item });
editingItem = normalizeItemData(JSON.parse(JSON.stringify(lineData.full_item)));
// Guardar una copia del estado original para restaurar al cancelar
originalItemData = JSON.parse(JSON.stringify(editingItem));
// Enrich with descriptive data
enrichItemData(editingItem);
showItemSheet = true;
@@ -563,6 +566,65 @@
}
function saveItem() {
// Validar campos obligatorios antes de guardar
const line = editingItem.lines?.[0];
const missingFields: string[] = [];
if (!line) {
toast.warning('Error de datos', {
description: 'No se encontró información de la línea del item'
});
return;
}
// 1. Clase
if (!line.class_id) {
missingFields.push('Clase');
}
// 2. Cantidad
if (!line.quantity?.quantity || line.quantity.quantity <= 0) {
missingFields.push('Cantidad');
}
// 3. Unidad de Medida
if (!line.unit_of_measure) {
missingFields.push('U.M. (Unidad de Medida)');
}
// 4. Costo Unitario (al menos uno debe estar presente)
const hasCost = line.financial?.unit_cost_usd ||
line.financial?.unit_cost_mxn ||
line.financial?.unit_cost_capture;
if (!hasCost) {
missingFields.push('Costo Unitario (USD, MXN o Captura)');
}
// 5. País de Origen
if (!line.customs?.origin_country) {
missingFields.push('País de Origen');
}
// 6. Tipo de Tarifa
if (!line.customs?.fraction_type) {
missingFields.push('Tipo de Tarifa');
}
// 7. Descripción en Español
if (!line.description?.description_spanish?.trim()) {
missingFields.push('Descripción en Español');
}
if (missingFields.length > 0) {
const fieldsList = missingFields.join('\n• ');
toast.warning('Completa los campos obligatorios', {
description: `Faltan los siguientes campos:\n• ${fieldsList}`,
duration: 10000
});
return;
}
// Si pasa la validación, continuar con el guardado
if (isEditMode) {
saveEditedItem();
} else {
@@ -594,6 +656,15 @@
isSaving = false;
}
}
function handleCancelEdit() {
// Restaurar los datos originales si estamos editando
if (isEditMode && originalItemData) {
editingItem = JSON.parse(JSON.stringify(originalItemData));
}
// Cerrar el sheet
showItemSheet = false;
}
</script>
<div class="grid grid-cols-4 grid-rows-1 gap-3">
@@ -715,6 +786,7 @@
bind:editingItem={editingItem}
{invoice}
onSave={saveItem}
onCancel={handleCancelEdit}
{isSaving}
/>
{:else}
@@ -724,6 +796,7 @@
bind:editingItem={editingItem}
{invoice}
onSave={saveItem}
onCancel={handleCancelEdit}
{isSaving}
/>
{/if}