feauture/catalogo-clases-inventario-UME
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
"""add stock_unit_of_measure to classes
|
||||
|
||||
Revision ID: c8d9e0f1a2b3
|
||||
Revises: b3c4d5e6f7a8
|
||||
Create Date: 2026-06-02 12:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "c8d9e0f1a2b3"
|
||||
down_revision: Union[str, None] = "b3c4d5e6f7a8"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"classes",
|
||||
sa.Column("stock_unit_of_measure", sa.String(length=5), nullable=True),
|
||||
schema="a76",
|
||||
)
|
||||
op.create_foreign_key(
|
||||
"fk_classes_stock_uom",
|
||||
"classes",
|
||||
"units_of_measure",
|
||||
["stock_unit_of_measure", "tenant_id", "company_id"],
|
||||
["code", "tenant_id", "company_id"],
|
||||
source_schema="a76",
|
||||
referent_schema="a76",
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_constraint(
|
||||
"fk_classes_stock_uom",
|
||||
"classes",
|
||||
schema="a76",
|
||||
type_="foreignkey",
|
||||
)
|
||||
op.drop_column("classes", "stock_unit_of_measure", schema="a76")
|
||||
@@ -28,6 +28,11 @@ class ClassCreateDTO(BaseModel):
|
||||
unit_of_measure: str = Field(
|
||||
..., max_length=5, description="Unit of measure - U.M. comercial (required)"
|
||||
)
|
||||
stock_unit_of_measure: Optional[str] = Field(
|
||||
None,
|
||||
max_length=5,
|
||||
description="UM Existencia - solo SCAII/inventory (UMEXISTENCIA)",
|
||||
)
|
||||
fraction: str = Field(
|
||||
..., max_length=20, description="Mexican tariff fraction (required)"
|
||||
)
|
||||
@@ -111,6 +116,11 @@ class ClassUpdateDTO(BaseModel):
|
||||
unit_of_measure: Optional[str] = Field(
|
||||
None, max_length=5, description="Unit of measure (homologated UNIMEDIDA)"
|
||||
)
|
||||
stock_unit_of_measure: Optional[str] = Field(
|
||||
None,
|
||||
max_length=5,
|
||||
description="UM Existencia - solo SCAII/inventory (UMEXISTENCIA)",
|
||||
)
|
||||
fraction: Optional[str] = Field(
|
||||
None, max_length=20, description="Mexican tariff fraction"
|
||||
)
|
||||
@@ -148,6 +158,7 @@ class ClassResponseDTO(BaseModel):
|
||||
description_en: Optional[str] = None
|
||||
material_key: Optional[str] = None
|
||||
unit_of_measure: Optional[str] = None
|
||||
stock_unit_of_measure: Optional[str] = None
|
||||
fraction: Optional[str] = None
|
||||
us_fraction: Optional[str] = None
|
||||
sub_key: Optional[str] = None
|
||||
@@ -230,6 +241,7 @@ class ClassWithFADataResponse(BaseModel):
|
||||
description_en: Optional[str] = None
|
||||
material_key: Optional[str] = None
|
||||
unit_of_measure: Optional[str] = None
|
||||
stock_unit_of_measure: Optional[str] = None
|
||||
fraction: Optional[str] = None
|
||||
us_fraction: Optional[str] = None
|
||||
sub_key: Optional[str] = None
|
||||
|
||||
@@ -42,6 +42,12 @@ class Class(Base, TenantScopedMixin, TimestampMixin):
|
||||
["a76.units_of_measure.code", "a76.units_of_measure.tenant_id",
|
||||
"a76.units_of_measure.company_id"],
|
||||
),
|
||||
ForeignKeyConstraint(
|
||||
["stock_unit_of_measure", "tenant_id", "company_id"],
|
||||
["a76.units_of_measure.code", "a76.units_of_measure.tenant_id",
|
||||
"a76.units_of_measure.company_id"],
|
||||
name="fk_classes_stock_uom",
|
||||
),
|
||||
UniqueConstraint(
|
||||
"tenant_id",
|
||||
"company_id",
|
||||
@@ -69,6 +75,9 @@ class Class(Base, TenantScopedMixin, TimestampMixin):
|
||||
unit_of_measure: Mapped[Optional[str]] = mapped_column(
|
||||
String(5)
|
||||
) # UNIMED - homologated from UNIMEDIDA
|
||||
stock_unit_of_measure: Mapped[Optional[str]] = mapped_column(
|
||||
String(5)
|
||||
) # UMEXISTENCIA - UM Existencia (SCAII/inventory)
|
||||
|
||||
# Tariff fractions
|
||||
fraction: Mapped[Optional[str]] = mapped_column(String(20)) # FRACCION
|
||||
@@ -97,7 +106,11 @@ class Class(Base, TenantScopedMixin, TimestampMixin):
|
||||
)
|
||||
unit_of_measure_info: Mapped[Optional["UnitOfMeasure"]] = relationship(
|
||||
foreign_keys=[unit_of_measure]
|
||||
)
|
||||
)
|
||||
stock_unit_of_measure_info: Mapped[Optional["UnitOfMeasure"]] = relationship(
|
||||
foreign_keys=[stock_unit_of_measure],
|
||||
viewonly=True,
|
||||
)
|
||||
|
||||
# Inverse relationship with GParts that have this class
|
||||
parts: Mapped[list["Part"]] = relationship(
|
||||
|
||||
@@ -25,6 +25,51 @@ from .models import Class
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _validate_stock_uom_conversion(
|
||||
db: Session,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
system: str,
|
||||
stock_unit_of_measure: Optional[str],
|
||||
unit_of_measure: Optional[str],
|
||||
) -> None:
|
||||
"""SCAII: si UM Existencia ≠ UM Comercial, debe existir conversión en catálogo."""
|
||||
if system != "inventory":
|
||||
return
|
||||
|
||||
stock_um = (stock_unit_of_measure or "").strip().upper()
|
||||
commercial_um = (unit_of_measure or "").strip().upper()
|
||||
if not stock_um or not commercial_um or stock_um == commercial_um:
|
||||
return
|
||||
|
||||
from api.v1.modules.a76.general_catalogs.unit_conversions.models import (
|
||||
UnitConversion,
|
||||
)
|
||||
|
||||
conversion_exists = (
|
||||
db.query(UnitConversion)
|
||||
.filter(
|
||||
UnitConversion.tenant_id == tenant_id,
|
||||
UnitConversion.company_id == company_id,
|
||||
or_(
|
||||
(UnitConversion.from_unit_code == stock_um)
|
||||
& (UnitConversion.to_unit_code == commercial_um),
|
||||
(UnitConversion.from_unit_code == commercial_um)
|
||||
& (UnitConversion.to_unit_code == stock_um),
|
||||
),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not conversion_exists:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=(
|
||||
f"No existe un factor de conversión para '{stock_um}' ↔ '{commercial_um}'. "
|
||||
"Configúrelo primero en el catálogo de Conversiones de UM."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ClassService:
|
||||
"""Servicio para gestión de clases SCAII y SCAF"""
|
||||
|
||||
@@ -202,6 +247,7 @@ class ClassService:
|
||||
"description_en": base_class.description_en,
|
||||
"material_key": base_class.material_key,
|
||||
"unit_of_measure": base_class.unit_of_measure,
|
||||
"stock_unit_of_measure": base_class.stock_unit_of_measure,
|
||||
"fraction": base_class.fraction,
|
||||
"us_fraction": base_class.us_fraction,
|
||||
"sub_key": base_class.sub_key,
|
||||
@@ -273,7 +319,16 @@ class ClassService:
|
||||
status_code=400,
|
||||
detail=f"Material type '{data_dict['material_key']}' does not exist"
|
||||
)
|
||||
|
||||
|
||||
_validate_stock_uom_conversion(
|
||||
db,
|
||||
tenant_id,
|
||||
company_id,
|
||||
data_dict.get("system", "fixed_asset"),
|
||||
data_dict.get("stock_unit_of_measure"),
|
||||
data_dict.get("unit_of_measure"),
|
||||
)
|
||||
|
||||
class_obj = Class(**data_dict)
|
||||
class_obj.tenant_id = tenant_id
|
||||
class_obj.company_id = company_id
|
||||
@@ -306,7 +361,23 @@ class ClassService:
|
||||
return None
|
||||
|
||||
update_data = class_data.model_dump(exclude_unset=True)
|
||||
|
||||
|
||||
effective_system = update_data.get("system", class_obj.system)
|
||||
effective_stock_um = update_data.get(
|
||||
"stock_unit_of_measure", class_obj.stock_unit_of_measure
|
||||
)
|
||||
effective_commercial_um = update_data.get(
|
||||
"unit_of_measure", class_obj.unit_of_measure
|
||||
)
|
||||
_validate_stock_uom_conversion(
|
||||
db,
|
||||
tenant_id,
|
||||
company_id,
|
||||
effective_system,
|
||||
effective_stock_um,
|
||||
effective_commercial_um,
|
||||
)
|
||||
|
||||
# Validate material_key exists if provided
|
||||
if "material_key" in update_data and update_data["material_key"]:
|
||||
from api.v1.modules.public.reference_data.material_types.models import MaterialType
|
||||
@@ -438,7 +509,8 @@ class ClassService:
|
||||
# Extract base class fields
|
||||
base_fields = {
|
||||
"class_code", "description_es", "description_en",
|
||||
"material_key", "unit_of_measure", "fraction", "us_fraction",
|
||||
"material_key", "unit_of_measure", "stock_unit_of_measure",
|
||||
"fraction", "us_fraction",
|
||||
"sub_key", "physical_review", "iva_exempt_fraction", "system"
|
||||
}
|
||||
base_data = {k: v for k, v in class_data.model_dump().items() if k in base_fields}
|
||||
@@ -477,6 +549,7 @@ class ClassService:
|
||||
"description_en": base_class.description_en,
|
||||
"material_key": base_class.material_key,
|
||||
"unit_of_measure": base_class.unit_of_measure,
|
||||
"stock_unit_of_measure": base_class.stock_unit_of_measure,
|
||||
"fraction": base_class.fraction,
|
||||
"us_fraction": base_class.us_fraction,
|
||||
"sub_key": base_class.sub_key,
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface A76Class {
|
||||
description_en: string | null;
|
||||
material_key: string | null;
|
||||
unit_of_measure: string;
|
||||
stock_unit_of_measure?: string | null;
|
||||
fraction: string;
|
||||
us_fraction: string;
|
||||
sub_key: string;
|
||||
@@ -31,6 +32,7 @@ export interface A76ClassCreate {
|
||||
description_en?: string | null;
|
||||
material_key?: string | null;
|
||||
unit_of_measure?: string | null;
|
||||
stock_unit_of_measure?: string | null;
|
||||
fraction?: string | null;
|
||||
us_fraction?: string | null;
|
||||
sub_key?: string | null;
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
normalizeMexTariffDigitsStored
|
||||
} from "$lib/utils/mexican-tariff-fraction";
|
||||
|
||||
export function createColumns(): ColumnDef<A76Class>[] {
|
||||
return [
|
||||
export function createColumns(options?: { includeStockUom?: boolean }): ColumnDef<A76Class>[] {
|
||||
const baseColumns: ColumnDef<A76Class>[] = [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => {
|
||||
@@ -129,6 +129,24 @@ export function createColumns(): ColumnDef<A76Class>[] {
|
||||
return renderSnippet(umSnippet, { um: row.original.unit_of_measure });
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
const stockUomColumn: ColumnDef<A76Class> = {
|
||||
accessorKey: "stock_unit_of_measure",
|
||||
header: "U.M. Existencia",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const umSnippet = createRawSnippet<[{ um: string | null }]>((getProps) => {
|
||||
const { um } = getProps();
|
||||
return {
|
||||
render: () => `<div class="text-muted-foreground">${um || '—'}</div>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(umSnippet, { um: row.original.stock_unit_of_measure ?? null });
|
||||
}
|
||||
};
|
||||
|
||||
const tailColumns: ColumnDef<A76Class>[] = [
|
||||
{
|
||||
accessorKey: "fraction",
|
||||
header: m['common.col_fraction'](),
|
||||
@@ -162,6 +180,11 @@ export function createColumns(): ColumnDef<A76Class>[] {
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
if (options?.includeStockUom) {
|
||||
return [...baseColumns, stockUomColumn, ...tailColumns];
|
||||
}
|
||||
return [...baseColumns, ...tailColumns];
|
||||
}
|
||||
|
||||
export const columns = createColumns();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { systemStore } from '$lib/stores/system.svelte';
|
||||
import DataTable from '$lib/components/dashboard/goods/classes/data-table.svelte';
|
||||
import { columns } from '$lib/components/dashboard/goods/classes/columns';
|
||||
import { createColumns } from '$lib/components/dashboard/goods/classes/columns';
|
||||
import { currentUser } from '$lib/auth';
|
||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||
import {
|
||||
@@ -72,6 +72,7 @@
|
||||
const activeSystem = $derived(systemStore.activeSystem);
|
||||
const isInventoryMode = $derived(activeSystem === 'inventory');
|
||||
const isFixedAssetMode = $derived(activeSystem !== 'inventory');
|
||||
const tableColumns = $derived(createColumns({ includeStockUom: isInventoryMode }));
|
||||
|
||||
// Permisos
|
||||
const canView = $derived(canViewGoodsClasses($currentUser));
|
||||
@@ -494,7 +495,7 @@
|
||||
<div class="min-h-0 flex-1 overflow-hidden">
|
||||
<DataTable
|
||||
data={classes}
|
||||
{columns}
|
||||
columns={tableColumns}
|
||||
loading={isLoading}
|
||||
selectedIds={selectedClassIds}
|
||||
onSelectedIdsChange={handleSelectedIdsChange}
|
||||
@@ -756,6 +757,12 @@
|
||||
description_en: cleanData.description_en?.trim() || '',
|
||||
material_key: cleanData.material_key?.trim() || '',
|
||||
unit_of_measure: cleanData.unit_of_measure?.trim() || '',
|
||||
...(isInventoryMode
|
||||
? {
|
||||
stock_unit_of_measure:
|
||||
cleanData.stock_unit_of_measure?.trim() || null
|
||||
}
|
||||
: {}),
|
||||
fraction: mxFraccionNorm,
|
||||
us_fraction: cleanData.us_fraction || '',
|
||||
physical_review: cleanData.physical_review ? 1 : 0,
|
||||
@@ -825,6 +832,12 @@
|
||||
description_en: cleanData.description_en?.trim() || '',
|
||||
material_key: cleanData.material_key?.trim() || '',
|
||||
unit_of_measure: cleanData.unit_of_measure?.trim() || '',
|
||||
...(isInventoryMode
|
||||
? {
|
||||
stock_unit_of_measure:
|
||||
cleanData.stock_unit_of_measure?.trim() || null
|
||||
}
|
||||
: {}),
|
||||
fraction: mxFraccionNorm,
|
||||
us_fraction: cleanData.us_fraction?.trim() || '',
|
||||
sub_key: cleanData.sub_key || '',
|
||||
|
||||
Reference in New Issue
Block a user