Merge pull request 'fix/desc-clase' (#69) from fix/desc-clase into fix/id-items
Reviewed-on: ADUANASOFT/anexo76#69
This commit is contained in:
@@ -33,14 +33,8 @@ def validate_create(
|
||||
code="REQUIRED"
|
||||
)
|
||||
|
||||
# 2. Validar part_number_id
|
||||
if not line.part_number_id:
|
||||
errors.add_error(
|
||||
field="part_number_id",
|
||||
message="Part Number ID es obligatorio",
|
||||
solution="Selecciona un número de parte válido del catálogo",
|
||||
code="REQUIRED"
|
||||
)
|
||||
# 2. Validar part_number_id (OPCIONAL - ya no es obligatorio)
|
||||
# El campo part_number_id ahora es opcional
|
||||
|
||||
# 3. Validar class_id
|
||||
if not line.class_id:
|
||||
@@ -115,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"
|
||||
)
|
||||
@@ -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:
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
<!-- Descriptions -->
|
||||
<fieldset class="border rounded-md p-2 space-y-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="num_parte" class="text-xs">Número de Parte: <span class="text-red-500">*</span></Label>
|
||||
<Label for="num_parte" class="text-xs">Número de Parte:</Label>
|
||||
<div class="flex gap-1">
|
||||
<Input
|
||||
id="num_parte"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import TariffFractionDialog from './tariff-fraction-dialog.svelte';
|
||||
import ClassDialog from './class-dialog.svelte';
|
||||
import type { LineItem, LineQuantities, LineFinancials, LineCustoms } from '$lib/api/dashboard/a76/items';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
let {
|
||||
lineItem = $bindable(),
|
||||
@@ -26,6 +27,9 @@
|
||||
let showCountryDialog = $state(false);
|
||||
let showFractionDialog = $state(false);
|
||||
|
||||
// Track previous class_id to detect changes
|
||||
let previousClassId = $state<number | undefined>(undefined);
|
||||
|
||||
// Initialize from existing data
|
||||
$effect(() => {
|
||||
if (lineItem.class_code) {
|
||||
@@ -33,12 +37,66 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Watch for class_id changes and update descriptions automatically
|
||||
$effect(() => {
|
||||
const currentClassId = lineItem.class_id;
|
||||
const activeCompanyId = companyStore?.activeCompany?.id;
|
||||
|
||||
// Only fetch if class_id changed, is valid, and we have a company
|
||||
if (currentClassId && currentClassId !== previousClassId && activeCompanyId) {
|
||||
previousClassId = currentClassId;
|
||||
|
||||
// Fetch all classes and find the one with matching ID
|
||||
fetch(`/api-sveltekit/classes?company_id=${activeCompanyId}&limit=100`)
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
throw new Error('Failed to fetch classes');
|
||||
})
|
||||
.then(data => {
|
||||
const classes = data.items || [];
|
||||
const classItem = classes.find((c: any) => c.id === currentClassId);
|
||||
|
||||
if (classItem) {
|
||||
// Store the code and description in the lineItem for display
|
||||
(lineItem as any).class_code = classItem.class_code;
|
||||
(lineItem as any).class_unit_of_measure = classItem.unit_of_measure;
|
||||
(lineItem as any).class_description = classItem.description_es || classItem.description_en;
|
||||
|
||||
// Update description fields if description object exists
|
||||
if ((lineItem as any).description) {
|
||||
if (classItem.description_es) {
|
||||
(lineItem as any).description.description_spanish = classItem.description_es;
|
||||
}
|
||||
if (classItem.description_en) {
|
||||
(lineItem as any).description.description_english = classItem.description_en;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching class data:', error);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function handleClassSelect(classItem: any) {
|
||||
lineItem.class_id = classItem.id;
|
||||
// Store the unit of measure and description for display
|
||||
(lineItem as any).class_unit_of_measure = classItem.unit_of_measure;
|
||||
(lineItem as any).class_code = classItem.class_code;
|
||||
(lineItem as any).class_description = classItem.description_es || classItem.description_en;
|
||||
|
||||
// Update description fields if description object exists
|
||||
if ((lineItem as any).description) {
|
||||
if (classItem.description_es) {
|
||||
(lineItem as any).description.description_spanish = classItem.description_es;
|
||||
}
|
||||
if (classItem.description_en) {
|
||||
(lineItem as any).description.description_english = classItem.description_en;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleUnitSelect(unit: any) {
|
||||
@@ -194,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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user