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(
|
unit_of_measure: str = Field(
|
||||||
..., max_length=5, description="Unit of measure - U.M. comercial (required)"
|
..., 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(
|
fraction: str = Field(
|
||||||
..., max_length=20, description="Mexican tariff fraction (required)"
|
..., max_length=20, description="Mexican tariff fraction (required)"
|
||||||
)
|
)
|
||||||
@@ -111,6 +116,11 @@ class ClassUpdateDTO(BaseModel):
|
|||||||
unit_of_measure: Optional[str] = Field(
|
unit_of_measure: Optional[str] = Field(
|
||||||
None, max_length=5, description="Unit of measure (homologated UNIMEDIDA)"
|
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(
|
fraction: Optional[str] = Field(
|
||||||
None, max_length=20, description="Mexican tariff fraction"
|
None, max_length=20, description="Mexican tariff fraction"
|
||||||
)
|
)
|
||||||
@@ -148,6 +158,7 @@ class ClassResponseDTO(BaseModel):
|
|||||||
description_en: Optional[str] = None
|
description_en: Optional[str] = None
|
||||||
material_key: Optional[str] = None
|
material_key: Optional[str] = None
|
||||||
unit_of_measure: Optional[str] = None
|
unit_of_measure: Optional[str] = None
|
||||||
|
stock_unit_of_measure: Optional[str] = None
|
||||||
fraction: Optional[str] = None
|
fraction: Optional[str] = None
|
||||||
us_fraction: Optional[str] = None
|
us_fraction: Optional[str] = None
|
||||||
sub_key: Optional[str] = None
|
sub_key: Optional[str] = None
|
||||||
@@ -230,6 +241,7 @@ class ClassWithFADataResponse(BaseModel):
|
|||||||
description_en: Optional[str] = None
|
description_en: Optional[str] = None
|
||||||
material_key: Optional[str] = None
|
material_key: Optional[str] = None
|
||||||
unit_of_measure: Optional[str] = None
|
unit_of_measure: Optional[str] = None
|
||||||
|
stock_unit_of_measure: Optional[str] = None
|
||||||
fraction: Optional[str] = None
|
fraction: Optional[str] = None
|
||||||
us_fraction: Optional[str] = None
|
us_fraction: Optional[str] = None
|
||||||
sub_key: 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.code", "a76.units_of_measure.tenant_id",
|
||||||
"a76.units_of_measure.company_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(
|
UniqueConstraint(
|
||||||
"tenant_id",
|
"tenant_id",
|
||||||
"company_id",
|
"company_id",
|
||||||
@@ -69,6 +75,9 @@ class Class(Base, TenantScopedMixin, TimestampMixin):
|
|||||||
unit_of_measure: Mapped[Optional[str]] = mapped_column(
|
unit_of_measure: Mapped[Optional[str]] = mapped_column(
|
||||||
String(5)
|
String(5)
|
||||||
) # UNIMED - homologated from UNIMEDIDA
|
) # UNIMED - homologated from UNIMEDIDA
|
||||||
|
stock_unit_of_measure: Mapped[Optional[str]] = mapped_column(
|
||||||
|
String(5)
|
||||||
|
) # UMEXISTENCIA - UM Existencia (SCAII/inventory)
|
||||||
|
|
||||||
# Tariff fractions
|
# Tariff fractions
|
||||||
fraction: Mapped[Optional[str]] = mapped_column(String(20)) # FRACCION
|
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(
|
unit_of_measure_info: Mapped[Optional["UnitOfMeasure"]] = relationship(
|
||||||
foreign_keys=[unit_of_measure]
|
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
|
# Inverse relationship with GParts that have this class
|
||||||
parts: Mapped[list["Part"]] = relationship(
|
parts: Mapped[list["Part"]] = relationship(
|
||||||
|
|||||||
@@ -25,6 +25,51 @@ from .models import Class
|
|||||||
logger = logging.getLogger(__name__)
|
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:
|
class ClassService:
|
||||||
"""Servicio para gestión de clases SCAII y SCAF"""
|
"""Servicio para gestión de clases SCAII y SCAF"""
|
||||||
|
|
||||||
@@ -202,6 +247,7 @@ class ClassService:
|
|||||||
"description_en": base_class.description_en,
|
"description_en": base_class.description_en,
|
||||||
"material_key": base_class.material_key,
|
"material_key": base_class.material_key,
|
||||||
"unit_of_measure": base_class.unit_of_measure,
|
"unit_of_measure": base_class.unit_of_measure,
|
||||||
|
"stock_unit_of_measure": base_class.stock_unit_of_measure,
|
||||||
"fraction": base_class.fraction,
|
"fraction": base_class.fraction,
|
||||||
"us_fraction": base_class.us_fraction,
|
"us_fraction": base_class.us_fraction,
|
||||||
"sub_key": base_class.sub_key,
|
"sub_key": base_class.sub_key,
|
||||||
@@ -273,7 +319,16 @@ class ClassService:
|
|||||||
status_code=400,
|
status_code=400,
|
||||||
detail=f"Material type '{data_dict['material_key']}' does not exist"
|
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 = Class(**data_dict)
|
||||||
class_obj.tenant_id = tenant_id
|
class_obj.tenant_id = tenant_id
|
||||||
class_obj.company_id = company_id
|
class_obj.company_id = company_id
|
||||||
@@ -306,7 +361,23 @@ class ClassService:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
update_data = class_data.model_dump(exclude_unset=True)
|
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
|
# Validate material_key exists if provided
|
||||||
if "material_key" in update_data and update_data["material_key"]:
|
if "material_key" in update_data and update_data["material_key"]:
|
||||||
from api.v1.modules.public.reference_data.material_types.models import MaterialType
|
from api.v1.modules.public.reference_data.material_types.models import MaterialType
|
||||||
@@ -438,7 +509,8 @@ class ClassService:
|
|||||||
# Extract base class fields
|
# Extract base class fields
|
||||||
base_fields = {
|
base_fields = {
|
||||||
"class_code", "description_es", "description_en",
|
"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"
|
"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}
|
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,
|
"description_en": base_class.description_en,
|
||||||
"material_key": base_class.material_key,
|
"material_key": base_class.material_key,
|
||||||
"unit_of_measure": base_class.unit_of_measure,
|
"unit_of_measure": base_class.unit_of_measure,
|
||||||
|
"stock_unit_of_measure": base_class.stock_unit_of_measure,
|
||||||
"fraction": base_class.fraction,
|
"fraction": base_class.fraction,
|
||||||
"us_fraction": base_class.us_fraction,
|
"us_fraction": base_class.us_fraction,
|
||||||
"sub_key": base_class.sub_key,
|
"sub_key": base_class.sub_key,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface A76Class {
|
|||||||
description_en: string | null;
|
description_en: string | null;
|
||||||
material_key: string | null;
|
material_key: string | null;
|
||||||
unit_of_measure: string;
|
unit_of_measure: string;
|
||||||
|
stock_unit_of_measure?: string | null;
|
||||||
fraction: string;
|
fraction: string;
|
||||||
us_fraction: string;
|
us_fraction: string;
|
||||||
sub_key: string;
|
sub_key: string;
|
||||||
@@ -31,6 +32,7 @@ export interface A76ClassCreate {
|
|||||||
description_en?: string | null;
|
description_en?: string | null;
|
||||||
material_key?: string | null;
|
material_key?: string | null;
|
||||||
unit_of_measure?: string | null;
|
unit_of_measure?: string | null;
|
||||||
|
stock_unit_of_measure?: string | null;
|
||||||
fraction?: string | null;
|
fraction?: string | null;
|
||||||
us_fraction?: string | null;
|
us_fraction?: string | null;
|
||||||
sub_key?: string | null;
|
sub_key?: string | null;
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import {
|
|||||||
normalizeMexTariffDigitsStored
|
normalizeMexTariffDigitsStored
|
||||||
} from "$lib/utils/mexican-tariff-fraction";
|
} from "$lib/utils/mexican-tariff-fraction";
|
||||||
|
|
||||||
export function createColumns(): ColumnDef<A76Class>[] {
|
export function createColumns(options?: { includeStockUom?: boolean }): ColumnDef<A76Class>[] {
|
||||||
return [
|
const baseColumns: ColumnDef<A76Class>[] = [
|
||||||
{
|
{
|
||||||
id: "select",
|
id: "select",
|
||||||
header: ({ table }) => {
|
header: ({ table }) => {
|
||||||
@@ -129,6 +129,24 @@ export function createColumns(): ColumnDef<A76Class>[] {
|
|||||||
return renderSnippet(umSnippet, { um: row.original.unit_of_measure });
|
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",
|
accessorKey: "fraction",
|
||||||
header: m['common.col_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();
|
export const columns = createColumns();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
type MaterialType
|
type MaterialType
|
||||||
} from '$lib/api/dashboard/reference_data/material_types';
|
} from '$lib/api/dashboard/reference_data/material_types';
|
||||||
import { unitsOfMeasureApi } from '$lib/api/dashboard/a76/units_of_measure';
|
import { unitsOfMeasureApi } from '$lib/api/dashboard/a76/units_of_measure';
|
||||||
|
import { unitConversionsApi } from '$lib/api/dashboard/a76/general_catalogs/unit-conversions';
|
||||||
import {
|
import {
|
||||||
getTariffFractions,
|
getTariffFractions,
|
||||||
type TariffFraction
|
type TariffFraction
|
||||||
@@ -70,6 +71,7 @@ const isInventoryMode = $derived(mode === 'inventory');
|
|||||||
description_en: initialData?.description_en || '',
|
description_en: initialData?.description_en || '',
|
||||||
material_key: initialData?.material_key || '',
|
material_key: initialData?.material_key || '',
|
||||||
unit_of_measure: initialData?.unit_of_measure || '',
|
unit_of_measure: initialData?.unit_of_measure || '',
|
||||||
|
stock_unit_of_measure: initialData?.stock_unit_of_measure || '',
|
||||||
fraction: initialData?.fraction || '',
|
fraction: initialData?.fraction || '',
|
||||||
us_fraction: initialData?.us_fraction || '',
|
us_fraction: initialData?.us_fraction || '',
|
||||||
annual_depreciation_rate: initialData?.annual_depreciation_rate || '',
|
annual_depreciation_rate: initialData?.annual_depreciation_rate || '',
|
||||||
@@ -86,6 +88,7 @@ const isInventoryMode = $derived(mode === 'inventory');
|
|||||||
// Campos calculados/display
|
// Campos calculados/display
|
||||||
material_description: '',
|
material_description: '',
|
||||||
unit_of_measure_description: '',
|
unit_of_measure_description: '',
|
||||||
|
stock_unit_of_measure_description: '',
|
||||||
unit_measure_key: '',
|
unit_measure_key: '',
|
||||||
fraction_umt: '',
|
fraction_umt: '',
|
||||||
fraction_uma_key: '',
|
fraction_uma_key: '',
|
||||||
@@ -115,6 +118,7 @@ const isInventoryMode = $derived(mode === 'inventory');
|
|||||||
formData.description_en = snap.description_en ?? '';
|
formData.description_en = snap.description_en ?? '';
|
||||||
formData.material_key = snap.material_key ?? '';
|
formData.material_key = snap.material_key ?? '';
|
||||||
formData.unit_of_measure = snap.unit_of_measure ?? '';
|
formData.unit_of_measure = snap.unit_of_measure ?? '';
|
||||||
|
formData.stock_unit_of_measure = snap.stock_unit_of_measure ?? '';
|
||||||
const mxDigits = normalizeMexTariffDigitsStored(
|
const mxDigits = normalizeMexTariffDigitsStored(
|
||||||
snap.import_tariff_code || snap.fraction || ''
|
snap.import_tariff_code || snap.fraction || ''
|
||||||
);
|
);
|
||||||
@@ -155,6 +159,7 @@ const isInventoryMode = $derived(mode === 'inventory');
|
|||||||
formData.description_en = '';
|
formData.description_en = '';
|
||||||
formData.material_key = '';
|
formData.material_key = '';
|
||||||
formData.unit_of_measure = '';
|
formData.unit_of_measure = '';
|
||||||
|
formData.stock_unit_of_measure = '';
|
||||||
formData.fraction = '';
|
formData.fraction = '';
|
||||||
formData.us_fraction = '';
|
formData.us_fraction = '';
|
||||||
formData.annual_depreciation_rate = '';
|
formData.annual_depreciation_rate = '';
|
||||||
@@ -192,6 +197,9 @@ const isInventoryMode = $derived(mode === 'inventory');
|
|||||||
|
|
||||||
let showUnitDialog = $state(false);
|
let showUnitDialog = $state(false);
|
||||||
let searchUnit = $state('');
|
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';
|
import TariffFractionSelector from '$lib/components/dashboard/goods/modales/TariffFractionSelector.svelte';
|
||||||
|
|
||||||
@@ -265,7 +273,76 @@ const isInventoryMode = $derived(mode === 'inventory');
|
|||||||
: unitsOfMeasureData
|
: 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
|
// 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() {
|
async function loadUnitsOfMeasure() {
|
||||||
if (unitsOfMeasureData.length > 0) return;
|
if (unitsOfMeasureData.length > 0) return;
|
||||||
try {
|
try {
|
||||||
@@ -346,15 +423,31 @@ const isInventoryMode = $derived(mode === 'inventory');
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openUnitOfMeasureSearch() {
|
async function openUnitOfMeasureSearch() {
|
||||||
|
unitPickerTarget = 'commercial';
|
||||||
|
showUnitDialog = true;
|
||||||
|
searchUnit = '';
|
||||||
|
await loadUnitsOfMeasure();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openStockUomSearch() {
|
||||||
|
unitPickerTarget = 'stock';
|
||||||
showUnitDialog = true;
|
showUnitDialog = true;
|
||||||
searchUnit = '';
|
searchUnit = '';
|
||||||
await loadUnitsOfMeasure();
|
await loadUnitsOfMeasure();
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectUnit(unit: UnitOfMeasure) {
|
function selectUnit(unit: UnitOfMeasure) {
|
||||||
formData.unit_of_measure = unit.code;
|
if (unitPickerTarget === 'stock') {
|
||||||
formData.unit_of_measure_description = unit.description;
|
formData.stock_unit_of_measure = unit.code;
|
||||||
formData.unit_measure_key = unit.claveMexicana;
|
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;
|
showUnitDialog = false;
|
||||||
searchUnit = '';
|
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)
|
// Fix 2: Máscara para Fracción Americana (formato 0000.00.00.00 = 13 chars)
|
||||||
function formatUSFraction(event: Event) {
|
function formatUSFraction(event: Event) {
|
||||||
const input = event.target as HTMLInputElement;
|
const input = event.target as HTMLInputElement;
|
||||||
@@ -544,6 +655,18 @@ const isInventoryMode = $derived(mode === 'inventory');
|
|||||||
searchCartaPorte = '';
|
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
|
// Función de validación
|
||||||
function validateForm(): boolean {
|
function validateForm(): boolean {
|
||||||
const errors: Record<string, string> = {};
|
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';
|
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()) {
|
if (!formData.fraction?.trim()) {
|
||||||
errors.fraction = 'La fracción arancelaria es obligatoria';
|
errors.fraction = 'La fracción arancelaria es obligatoria';
|
||||||
}
|
}
|
||||||
@@ -611,6 +741,14 @@ const isInventoryMode = $derived(mode === 'inventory');
|
|||||||
} else {
|
} else {
|
||||||
delete errors.unit_of_measure;
|
delete errors.unit_of_measure;
|
||||||
}
|
}
|
||||||
|
if (isInventoryMode) {
|
||||||
|
validateStockUnitOfMeasureField(errors);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'stock_unit_of_measure':
|
||||||
|
if (isInventoryMode) {
|
||||||
|
validateStockUnitOfMeasureField(errors);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'fraction':
|
case 'fraction':
|
||||||
if (!formData.fraction?.trim()) {
|
if (!formData.fraction?.trim()) {
|
||||||
@@ -625,6 +763,10 @@ const isInventoryMode = $derived(mode === 'inventory');
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
|
if (isInventoryMode && stockUmcDiffer) {
|
||||||
|
await checkUnitConversionPair();
|
||||||
|
}
|
||||||
|
|
||||||
// Validar formulario
|
// Validar formulario
|
||||||
if (!validateForm()) {
|
if (!validateForm()) {
|
||||||
showErrors = true;
|
showErrors = true;
|
||||||
@@ -789,6 +931,48 @@ const isInventoryMode = $derived(mode === 'inventory');
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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">
|
<div class="space-y-2">
|
||||||
<Label for="fraction" class="font-bold">
|
<Label for="fraction" class="font-bold">
|
||||||
Fracción Arancelaria: <span class="text-red-500">*</span>
|
Fracción Arancelaria: <span class="text-red-500">*</span>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
import { companyStore } from '$lib/stores/company.svelte';
|
import { companyStore } from '$lib/stores/company.svelte';
|
||||||
import { systemStore } from '$lib/stores/system.svelte';
|
import { systemStore } from '$lib/stores/system.svelte';
|
||||||
import DataTable from '$lib/components/dashboard/goods/classes/data-table.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 { currentUser } from '$lib/auth';
|
||||||
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
|
||||||
import {
|
import {
|
||||||
@@ -72,6 +72,7 @@
|
|||||||
const activeSystem = $derived(systemStore.activeSystem);
|
const activeSystem = $derived(systemStore.activeSystem);
|
||||||
const isInventoryMode = $derived(activeSystem === 'inventory');
|
const isInventoryMode = $derived(activeSystem === 'inventory');
|
||||||
const isFixedAssetMode = $derived(activeSystem !== 'inventory');
|
const isFixedAssetMode = $derived(activeSystem !== 'inventory');
|
||||||
|
const tableColumns = $derived(createColumns({ includeStockUom: isInventoryMode }));
|
||||||
|
|
||||||
// Permisos
|
// Permisos
|
||||||
const canView = $derived(canViewGoodsClasses($currentUser));
|
const canView = $derived(canViewGoodsClasses($currentUser));
|
||||||
@@ -494,7 +495,7 @@
|
|||||||
<div class="min-h-0 flex-1 overflow-hidden">
|
<div class="min-h-0 flex-1 overflow-hidden">
|
||||||
<DataTable
|
<DataTable
|
||||||
data={classes}
|
data={classes}
|
||||||
{columns}
|
columns={tableColumns}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
selectedIds={selectedClassIds}
|
selectedIds={selectedClassIds}
|
||||||
onSelectedIdsChange={handleSelectedIdsChange}
|
onSelectedIdsChange={handleSelectedIdsChange}
|
||||||
@@ -756,6 +757,12 @@
|
|||||||
description_en: cleanData.description_en?.trim() || '',
|
description_en: cleanData.description_en?.trim() || '',
|
||||||
material_key: cleanData.material_key?.trim() || '',
|
material_key: cleanData.material_key?.trim() || '',
|
||||||
unit_of_measure: cleanData.unit_of_measure?.trim() || '',
|
unit_of_measure: cleanData.unit_of_measure?.trim() || '',
|
||||||
|
...(isInventoryMode
|
||||||
|
? {
|
||||||
|
stock_unit_of_measure:
|
||||||
|
cleanData.stock_unit_of_measure?.trim() || null
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
fraction: mxFraccionNorm,
|
fraction: mxFraccionNorm,
|
||||||
us_fraction: cleanData.us_fraction || '',
|
us_fraction: cleanData.us_fraction || '',
|
||||||
physical_review: cleanData.physical_review ? 1 : 0,
|
physical_review: cleanData.physical_review ? 1 : 0,
|
||||||
@@ -825,6 +832,12 @@
|
|||||||
description_en: cleanData.description_en?.trim() || '',
|
description_en: cleanData.description_en?.trim() || '',
|
||||||
material_key: cleanData.material_key?.trim() || '',
|
material_key: cleanData.material_key?.trim() || '',
|
||||||
unit_of_measure: cleanData.unit_of_measure?.trim() || '',
|
unit_of_measure: cleanData.unit_of_measure?.trim() || '',
|
||||||
|
...(isInventoryMode
|
||||||
|
? {
|
||||||
|
stock_unit_of_measure:
|
||||||
|
cleanData.stock_unit_of_measure?.trim() || null
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
fraction: mxFraccionNorm,
|
fraction: mxFraccionNorm,
|
||||||
us_fraction: cleanData.us_fraction?.trim() || '',
|
us_fraction: cleanData.us_fraction?.trim() || '',
|
||||||
sub_key: cleanData.sub_key || '',
|
sub_key: cleanData.sub_key || '',
|
||||||
|
|||||||
Reference in New Issue
Block a user