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,
|
||||
|
||||
Reference in New Issue
Block a user