|
|
|
|
@@ -10,6 +10,7 @@
|
|
|
|
|
type MaterialType
|
|
|
|
|
} from '$lib/api/dashboard/reference_data/material_types';
|
|
|
|
|
import { unitsOfMeasureApi } from '$lib/api/dashboard/a76/units_of_measure';
|
|
|
|
|
import { unitConversionsApi } from '$lib/api/dashboard/a76/general_catalogs/unit-conversions';
|
|
|
|
|
import {
|
|
|
|
|
getTariffFractions,
|
|
|
|
|
type TariffFraction
|
|
|
|
|
@@ -70,6 +71,7 @@ const isInventoryMode = $derived(mode === 'inventory');
|
|
|
|
|
description_en: initialData?.description_en || '',
|
|
|
|
|
material_key: initialData?.material_key || '',
|
|
|
|
|
unit_of_measure: initialData?.unit_of_measure || '',
|
|
|
|
|
stock_unit_of_measure: initialData?.stock_unit_of_measure || '',
|
|
|
|
|
fraction: initialData?.fraction || '',
|
|
|
|
|
us_fraction: initialData?.us_fraction || '',
|
|
|
|
|
annual_depreciation_rate: initialData?.annual_depreciation_rate || '',
|
|
|
|
|
@@ -86,6 +88,7 @@ const isInventoryMode = $derived(mode === 'inventory');
|
|
|
|
|
// Campos calculados/display
|
|
|
|
|
material_description: '',
|
|
|
|
|
unit_of_measure_description: '',
|
|
|
|
|
stock_unit_of_measure_description: '',
|
|
|
|
|
unit_measure_key: '',
|
|
|
|
|
fraction_umt: '',
|
|
|
|
|
fraction_uma_key: '',
|
|
|
|
|
@@ -115,6 +118,7 @@ const isInventoryMode = $derived(mode === 'inventory');
|
|
|
|
|
formData.description_en = snap.description_en ?? '';
|
|
|
|
|
formData.material_key = snap.material_key ?? '';
|
|
|
|
|
formData.unit_of_measure = snap.unit_of_measure ?? '';
|
|
|
|
|
formData.stock_unit_of_measure = snap.stock_unit_of_measure ?? '';
|
|
|
|
|
const mxDigits = normalizeMexTariffDigitsStored(
|
|
|
|
|
snap.import_tariff_code || snap.fraction || ''
|
|
|
|
|
);
|
|
|
|
|
@@ -155,6 +159,7 @@ const isInventoryMode = $derived(mode === 'inventory');
|
|
|
|
|
formData.description_en = '';
|
|
|
|
|
formData.material_key = '';
|
|
|
|
|
formData.unit_of_measure = '';
|
|
|
|
|
formData.stock_unit_of_measure = '';
|
|
|
|
|
formData.fraction = '';
|
|
|
|
|
formData.us_fraction = '';
|
|
|
|
|
formData.annual_depreciation_rate = '';
|
|
|
|
|
@@ -192,6 +197,9 @@ const isInventoryMode = $derived(mode === 'inventory');
|
|
|
|
|
|
|
|
|
|
let showUnitDialog = $state(false);
|
|
|
|
|
let searchUnit = $state('');
|
|
|
|
|
let unitPickerTarget = $state<'commercial' | 'stock'>('commercial');
|
|
|
|
|
let conversionExists = $state<boolean | null>(null);
|
|
|
|
|
let conversionCheckLoading = $state(false);
|
|
|
|
|
|
|
|
|
|
import TariffFractionSelector from '$lib/components/dashboard/goods/modales/TariffFractionSelector.svelte';
|
|
|
|
|
|
|
|
|
|
@@ -265,7 +273,76 @@ const isInventoryMode = $derived(mode === 'inventory');
|
|
|
|
|
: unitsOfMeasureData
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const stockUmcDiffer = $derived.by(() => {
|
|
|
|
|
if (!isInventoryMode) return false;
|
|
|
|
|
const stock = formData.stock_unit_of_measure?.trim().toUpperCase();
|
|
|
|
|
const commercial = formData.unit_of_measure?.trim().toUpperCase();
|
|
|
|
|
return Boolean(stock && commercial && stock !== commercial);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Funciones
|
|
|
|
|
async function checkUnitConversionPair(): Promise<void> {
|
|
|
|
|
if (!stockUmcDiffer) {
|
|
|
|
|
conversionExists = null;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const stockUm = formData.stock_unit_of_measure.trim().toUpperCase();
|
|
|
|
|
const commercialUm = formData.unit_of_measure.trim().toUpperCase();
|
|
|
|
|
const companyId = companyStore.activeCompany?.id;
|
|
|
|
|
if (!companyId) {
|
|
|
|
|
conversionExists = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
conversionCheckLoading = true;
|
|
|
|
|
try {
|
|
|
|
|
const [fwd, rev] = await Promise.all([
|
|
|
|
|
unitConversionsApi.list(1, 10, companyId, {
|
|
|
|
|
from_unit_code: stockUm,
|
|
|
|
|
to_unit_code: commercialUm
|
|
|
|
|
}),
|
|
|
|
|
unitConversionsApi.list(1, 10, companyId, {
|
|
|
|
|
from_unit_code: commercialUm,
|
|
|
|
|
to_unit_code: stockUm
|
|
|
|
|
})
|
|
|
|
|
]);
|
|
|
|
|
const fwdMatch = fwd.items.some(
|
|
|
|
|
(c) =>
|
|
|
|
|
c.from_unit_code.toUpperCase() === stockUm &&
|
|
|
|
|
c.to_unit_code.toUpperCase() === commercialUm
|
|
|
|
|
);
|
|
|
|
|
const revMatch = rev.items.some(
|
|
|
|
|
(c) =>
|
|
|
|
|
c.from_unit_code.toUpperCase() === commercialUm &&
|
|
|
|
|
c.to_unit_code.toUpperCase() === stockUm
|
|
|
|
|
);
|
|
|
|
|
conversionExists = fwdMatch || revMatch;
|
|
|
|
|
} catch {
|
|
|
|
|
conversionExists = false;
|
|
|
|
|
} finally {
|
|
|
|
|
conversionCheckLoading = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$effect(() => {
|
|
|
|
|
if (!isInventoryMode) {
|
|
|
|
|
conversionExists = null;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const stock = formData.stock_unit_of_measure;
|
|
|
|
|
const commercial = formData.unit_of_measure;
|
|
|
|
|
if (!stock?.trim() || !commercial?.trim()) {
|
|
|
|
|
conversionExists = null;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (stock.trim().toUpperCase() === commercial.trim().toUpperCase()) {
|
|
|
|
|
conversionExists = null;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
void checkUnitConversionPair();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
async function loadUnitsOfMeasure() {
|
|
|
|
|
if (unitsOfMeasureData.length > 0) return;
|
|
|
|
|
try {
|
|
|
|
|
@@ -346,15 +423,31 @@ const isInventoryMode = $derived(mode === 'inventory');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function openUnitOfMeasureSearch() {
|
|
|
|
|
unitPickerTarget = 'commercial';
|
|
|
|
|
showUnitDialog = true;
|
|
|
|
|
searchUnit = '';
|
|
|
|
|
await loadUnitsOfMeasure();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function openStockUomSearch() {
|
|
|
|
|
unitPickerTarget = 'stock';
|
|
|
|
|
showUnitDialog = true;
|
|
|
|
|
searchUnit = '';
|
|
|
|
|
await loadUnitsOfMeasure();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectUnit(unit: UnitOfMeasure) {
|
|
|
|
|
formData.unit_of_measure = unit.code;
|
|
|
|
|
formData.unit_of_measure_description = unit.description;
|
|
|
|
|
formData.unit_measure_key = unit.claveMexicana;
|
|
|
|
|
if (unitPickerTarget === 'stock') {
|
|
|
|
|
formData.stock_unit_of_measure = unit.code;
|
|
|
|
|
formData.stock_unit_of_measure_description = unit.description;
|
|
|
|
|
if (showErrors) validateField('stock_unit_of_measure');
|
|
|
|
|
} else {
|
|
|
|
|
formData.unit_of_measure = unit.code;
|
|
|
|
|
formData.unit_of_measure_description = unit.description;
|
|
|
|
|
formData.unit_measure_key = unit.claveMexicana;
|
|
|
|
|
if (showErrors && isInventoryMode) validateField('stock_unit_of_measure');
|
|
|
|
|
validateField('unit_of_measure');
|
|
|
|
|
}
|
|
|
|
|
showUnitDialog = false;
|
|
|
|
|
searchUnit = '';
|
|
|
|
|
}
|
|
|
|
|
@@ -379,6 +472,24 @@ const isInventoryMode = $derived(mode === 'inventory');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleStockUomBlur() {
|
|
|
|
|
if (!isInventoryMode) return;
|
|
|
|
|
validateField('stock_unit_of_measure');
|
|
|
|
|
const code = formData.stock_unit_of_measure?.trim().toUpperCase();
|
|
|
|
|
if (!code) {
|
|
|
|
|
formData.stock_unit_of_measure_description = '';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
await loadUnitsOfMeasure();
|
|
|
|
|
const found = unitsOfMeasureData.find((u) => u.code.toUpperCase() === code);
|
|
|
|
|
if (found) {
|
|
|
|
|
formData.stock_unit_of_measure = found.code;
|
|
|
|
|
formData.stock_unit_of_measure_description = found.description;
|
|
|
|
|
} else {
|
|
|
|
|
formData.stock_unit_of_measure_description = '(Código no encontrado)';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fix 2: Máscara para Fracción Americana (formato 0000.00.00.00 = 13 chars)
|
|
|
|
|
function formatUSFraction(event: Event) {
|
|
|
|
|
const input = event.target as HTMLInputElement;
|
|
|
|
|
@@ -544,6 +655,18 @@ const isInventoryMode = $derived(mode === 'inventory');
|
|
|
|
|
searchCartaPorte = '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateStockUnitOfMeasureField(errors: Record<string, string>) {
|
|
|
|
|
if (!formData.stock_unit_of_measure?.trim()) {
|
|
|
|
|
errors.stock_unit_of_measure = 'La unidad de medida de existencia es obligatoria';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
delete errors.stock_unit_of_measure;
|
|
|
|
|
if (stockUmcDiffer && conversionExists === false) {
|
|
|
|
|
errors.stock_unit_of_measure =
|
|
|
|
|
'No existe factor de conversión para este par. Configúrelo en el catálogo de Conversiones de UM.';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Función de validación
|
|
|
|
|
function validateForm(): boolean {
|
|
|
|
|
const errors: Record<string, string> = {};
|
|
|
|
|
@@ -566,6 +689,13 @@ const isInventoryMode = $derived(mode === 'inventory');
|
|
|
|
|
errors.unit_of_measure = 'La unidad de medida comercial es obligatoria';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isInventoryMode && !formData.stock_unit_of_measure?.trim()) {
|
|
|
|
|
errors.stock_unit_of_measure = 'La unidad de medida de existencia es obligatoria';
|
|
|
|
|
} else if (isInventoryMode && stockUmcDiffer && conversionExists === false) {
|
|
|
|
|
errors.stock_unit_of_measure =
|
|
|
|
|
'No existe factor de conversión para este par. Configúrelo en el catálogo de Conversiones de UM.';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!formData.fraction?.trim()) {
|
|
|
|
|
errors.fraction = 'La fracción arancelaria es obligatoria';
|
|
|
|
|
}
|
|
|
|
|
@@ -611,6 +741,14 @@ const isInventoryMode = $derived(mode === 'inventory');
|
|
|
|
|
} else {
|
|
|
|
|
delete errors.unit_of_measure;
|
|
|
|
|
}
|
|
|
|
|
if (isInventoryMode) {
|
|
|
|
|
validateStockUnitOfMeasureField(errors);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'stock_unit_of_measure':
|
|
|
|
|
if (isInventoryMode) {
|
|
|
|
|
validateStockUnitOfMeasureField(errors);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'fraction':
|
|
|
|
|
if (!formData.fraction?.trim()) {
|
|
|
|
|
@@ -625,6 +763,10 @@ const isInventoryMode = $derived(mode === 'inventory');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleSave() {
|
|
|
|
|
if (isInventoryMode && stockUmcDiffer) {
|
|
|
|
|
await checkUnitConversionPair();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validar formulario
|
|
|
|
|
if (!validateForm()) {
|
|
|
|
|
showErrors = true;
|
|
|
|
|
@@ -789,6 +931,48 @@ const isInventoryMode = $derived(mode === 'inventory');
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{#if isInventoryMode}
|
|
|
|
|
<div class="space-y-2 md:col-span-2">
|
|
|
|
|
<Label for="stock_unit_of_measure" class="font-bold">
|
|
|
|
|
U.M. Existencia: <span class="text-red-500">*</span>
|
|
|
|
|
</Label>
|
|
|
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
|
|
|
<Input
|
|
|
|
|
id="stock_unit_of_measure"
|
|
|
|
|
bind:value={formData.stock_unit_of_measure}
|
|
|
|
|
placeholder="UME"
|
|
|
|
|
class="min-w-[5rem] flex-1 uppercase {validationErrors.stock_unit_of_measure
|
|
|
|
|
? 'border-red-500 focus-visible:ring-red-500'
|
|
|
|
|
: ''}"
|
|
|
|
|
maxlength={5}
|
|
|
|
|
onblur={handleStockUomBlur}
|
|
|
|
|
/>
|
|
|
|
|
<Button type="button" variant="outline" size="icon" onclick={openStockUomSearch}>
|
|
|
|
|
<Folder class="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
<span class="text-sm text-blue-600">
|
|
|
|
|
{formData.stock_unit_of_measure_description || ''}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
{#if validationErrors.stock_unit_of_measure}
|
|
|
|
|
<p class="mt-1 text-sm text-red-500">{validationErrors.stock_unit_of_measure}</p>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if stockUmcDiffer}
|
|
|
|
|
{#if conversionCheckLoading}
|
|
|
|
|
<p class="text-sm text-muted-foreground">Verificando factor de conversión…</p>
|
|
|
|
|
{:else if conversionExists === false}
|
|
|
|
|
<p class="text-sm font-semibold text-red-600">
|
|
|
|
|
UM Existencia ≠ UM Comercial y no existe factor de conversión para este par.
|
|
|
|
|
Configúrelo primero en el catálogo de Conversiones de UM.
|
|
|
|
|
</p>
|
|
|
|
|
{:else if conversionExists === true}
|
|
|
|
|
<p class="text-sm text-green-600">Factor de conversión encontrado para este par.</p>
|
|
|
|
|
{/if}
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
<Label for="fraction" class="font-bold">
|
|
|
|
|
Fracción Arancelaria: <span class="text-red-500">*</span>
|
|
|
|
|
|