6feature/fix-en-base-a-scaii
This commit is contained in:
@@ -3,43 +3,17 @@ from decimal import Decimal
|
||||
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
|
||||
# Equivalency (parent) DTOs
|
||||
|
||||
|
||||
class EquivalencyBase(BaseModel):
|
||||
# Agrupa múltiples destinos para un mismo origen.
|
||||
identifier: str = Field(..., max_length=10, description="From unit code")
|
||||
description: Optional[str] = Field(None, max_length=200, description="Description (optional)")
|
||||
|
||||
|
||||
class EquivalencyCreate(EquivalencyBase):
|
||||
pass
|
||||
|
||||
|
||||
class EquivalencyUpdate(BaseModel):
|
||||
identifier: Optional[str] = Field(None, max_length=10)
|
||||
description: Optional[str] = Field(None, max_length=200)
|
||||
|
||||
|
||||
class EquivalencyResponse(EquivalencyBase):
|
||||
id: int
|
||||
tenant_id: int
|
||||
company_id: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
# Equivalency Item DTOs (catálogo por fila)
|
||||
# EquivalencyItem DTOs (pool global independiente)
|
||||
|
||||
|
||||
class EquivalencyItemBase(BaseModel):
|
||||
original_field: str = Field(..., max_length=100, description="Original unit code (from)")
|
||||
external_field: str = Field(..., max_length=100, description="External unit code (to)")
|
||||
conversion_factor: Optional[Decimal] = Field(None, description="Conversion factor")
|
||||
original_field: str = Field(..., max_length=100, description="Campo original (from)")
|
||||
external_field: str = Field(..., max_length=100, description="Campo externo (to)")
|
||||
conversion_factor: Optional[Decimal] = Field(None, description="Factor de conversión")
|
||||
|
||||
|
||||
class EquivalencyItemCreate(EquivalencyItemBase):
|
||||
conversion_factor: Decimal = Field(..., description="Conversion factor")
|
||||
conversion_factor: Optional[Decimal] = Field(None, description="Factor de conversión")
|
||||
|
||||
|
||||
class EquivalencyItemUpdate(BaseModel):
|
||||
@@ -50,8 +24,35 @@ class EquivalencyItemUpdate(BaseModel):
|
||||
|
||||
class EquivalencyItemResponse(EquivalencyItemBase):
|
||||
id: int
|
||||
equivalency_id: int
|
||||
tenant_id: int
|
||||
company_id: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
# Equivalency DTOs (catálogo — referencia un item del pool)
|
||||
|
||||
|
||||
class EquivalencyBase(BaseModel):
|
||||
identifier: str = Field(..., max_length=10, description="Identificador único")
|
||||
description: Optional[str] = Field(None, max_length=200, description="Descripción (opcional)")
|
||||
item_id: Optional[int] = Field(None, description="ID del EquivalencyItem seleccionado")
|
||||
|
||||
|
||||
class EquivalencyCreate(EquivalencyBase):
|
||||
pass
|
||||
|
||||
|
||||
class EquivalencyUpdate(BaseModel):
|
||||
identifier: Optional[str] = Field(None, max_length=10)
|
||||
description: Optional[str] = Field(None, max_length=200)
|
||||
item_id: Optional[int] = Field(None, description="Re-asociar a otro EquivalencyItem")
|
||||
|
||||
|
||||
class EquivalencyResponse(EquivalencyBase):
|
||||
id: int
|
||||
tenant_id: int
|
||||
company_id: int
|
||||
item: Optional[EquivalencyItemResponse] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -1,13 +1,39 @@
|
||||
from typing import Optional, List
|
||||
from decimal import Decimal
|
||||
from sqlalchemy import Integer, String, ForeignKey, UniqueConstraint, ForeignKeyConstraint, Numeric
|
||||
from sqlalchemy import Integer, String, ForeignKey, UniqueConstraint, Numeric
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure
|
||||
|
||||
|
||||
class EquivalencyItem(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""Pool global de mapeos campo_original → campo_externo.
|
||||
Un mismo item puede ser referenciado por múltiples Equivalency."""
|
||||
|
||||
__tablename__ = "equivalency_items"
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"original_field", "external_field", "tenant_id", "company_id",
|
||||
name="uq_equivalency_item_fields"
|
||||
),
|
||||
{"schema": "a76"}
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
original_field: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
external_field: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
conversion_factor: Mapped[Optional[Decimal]] = mapped_column(
|
||||
Numeric(13, 6), nullable=True)
|
||||
|
||||
equivalencies: Mapped[List["Equivalency"]] = relationship(
|
||||
back_populates="item", foreign_keys="[Equivalency.item_id]")
|
||||
|
||||
|
||||
class Equivalency(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""Catálogo de equivalencias. Cada registro selecciona UN item del pool."""
|
||||
|
||||
__tablename__ = "equivalencies"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("identifier", "tenant_id", "company_id",
|
||||
@@ -19,42 +45,13 @@ class Equivalency(Base, TenantScopedMixin, TimestampMixin):
|
||||
Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
identifier: Mapped[str] = mapped_column(String(10), nullable=False)
|
||||
|
||||
description: Mapped[Optional[str]] = mapped_column(
|
||||
String(200), nullable=True)
|
||||
|
||||
items: Mapped[List["EquivalencyItem"]] = relationship(
|
||||
back_populates="equivalency", cascade="all, delete-orphan")
|
||||
item_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("a76.equivalency_items.id", ondelete="SET NULL"),
|
||||
nullable=True)
|
||||
|
||||
|
||||
class EquivalencyItem(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "equivalency_items"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("equivalency_id", "original_field",
|
||||
"external_field", "tenant_id", "company_id", name="uq_equivalency_item_fields"),
|
||||
ForeignKeyConstraint(
|
||||
["original_field", "tenant_id", "company_id"],
|
||||
["a76.units_of_measure.code", "a76.units_of_measure.tenant_id",
|
||||
"a76.units_of_measure.company_id"]
|
||||
),
|
||||
{"schema": "a76"}
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
equivalency_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("a76.equivalencies.id"), nullable=False)
|
||||
|
||||
original_field: Mapped[str] = mapped_column(
|
||||
String(100), nullable=False) # Relation to Unit of Measure
|
||||
|
||||
external_field: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
|
||||
conversion_factor: Mapped[Optional[Decimal]] = mapped_column(
|
||||
Numeric(13, 6), nullable=True
|
||||
)
|
||||
|
||||
equivalency: Mapped["Equivalency"] = relationship(back_populates="items")
|
||||
|
||||
unit_of_measure: Mapped["UnitOfMeasure"] = relationship()
|
||||
item: Mapped[Optional["EquivalencyItem"]] = relationship(
|
||||
back_populates="equivalencies", foreign_keys=[item_id])
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Dict, Any
|
||||
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user, validate_access_to_resource
|
||||
from fastapi import APIRouter
|
||||
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
|
||||
from . import service
|
||||
from .models import Equivalency, EquivalencyItem
|
||||
from .dto import (
|
||||
EquivalencyCreate, EquivalencyResponse, EquivalencyUpdate,
|
||||
EquivalencyItemCreate, EquivalencyItemResponse, EquivalencyItemUpdate
|
||||
@@ -16,19 +9,7 @@ from .service import EquivalencyService, EquivalencyItemService
|
||||
router = APIRouter(prefix="/equivalencies",
|
||||
tags=["a76.general_catalogs.equivalencies"])
|
||||
|
||||
# Equivalency CRUD
|
||||
equivalency_crud = TenantCRUDRoutes(
|
||||
service=EquivalencyService,
|
||||
create_schema=EquivalencyCreate,
|
||||
update_schema=EquivalencyUpdate,
|
||||
response_schema=EquivalencyResponse,
|
||||
prefix="",
|
||||
tags=["Equivalencies"],
|
||||
resource_name="Equivalency",
|
||||
enable_list=True,
|
||||
)
|
||||
|
||||
# Equivalency Item CRUD
|
||||
# Pool global de EquivalencyItems
|
||||
item_crud = TenantCRUDRoutes(
|
||||
service=EquivalencyItemService,
|
||||
create_schema=EquivalencyItemCreate,
|
||||
@@ -41,60 +22,18 @@ item_crud = TenantCRUDRoutes(
|
||||
enable_filters=True,
|
||||
)
|
||||
|
||||
# Custom endpoint for creating items nested under equivalency
|
||||
|
||||
|
||||
@equivalency_crud.router.post(
|
||||
"/{equivalency_id}/items",
|
||||
response_model=EquivalencyItemResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Create equivalency item",
|
||||
# Catálogo de equivalencias (referencia item_id)
|
||||
equivalency_crud = TenantCRUDRoutes(
|
||||
service=EquivalencyService,
|
||||
create_schema=EquivalencyCreate,
|
||||
update_schema=EquivalencyUpdate,
|
||||
response_schema=EquivalencyResponse,
|
||||
prefix="",
|
||||
tags=["Equivalencies"],
|
||||
resource_name="Equivalency",
|
||||
enable_list=True,
|
||||
enable_filters=True,
|
||||
)
|
||||
async def create_equivalency_item(
|
||||
equivalency_id: int,
|
||||
data: EquivalencyItemCreate,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
# Verify parent exists
|
||||
parent = EquivalencyService.get_by_id(
|
||||
db, equivalency_id, tenant_id, company_id)
|
||||
if not parent:
|
||||
raise HTTPException(status_code=404, detail="Equivalency not found")
|
||||
|
||||
# Create item
|
||||
# We need to manually handle the creation because the DTO doesn't have equivalency_id
|
||||
# and the service.create expects data to match the model or DTO.
|
||||
# But service.create takes EquivalencyItemCreate which doesn't have equivalency_id.
|
||||
# So we need to modify the data or handle it in service.
|
||||
|
||||
# Actually, I implemented EquivalencyItemService.create to take EquivalencyItemCreate.
|
||||
# And it tries to create the model.
|
||||
# But the model needs equivalency_id.
|
||||
# So EquivalencyItemService.create will fail if I don't pass equivalency_id.
|
||||
# I should update EquivalencyItemService.create to accept extra kwargs or handle this.
|
||||
|
||||
# Let's update the service call here to pass equivalency_id manually if I can't change the service signature easily.
|
||||
# But wait, I can just instantiate the model here or update the service.
|
||||
|
||||
# I'll update the service to handle it.
|
||||
# But for now, let's assume I can pass it in the data if I convert it to dict.
|
||||
|
||||
item_data = data.model_dump()
|
||||
item_data['equivalency_id'] = equivalency_id
|
||||
|
||||
# I need to call a method that accepts this.
|
||||
# EquivalencyItemService.create takes EquivalencyItemCreate.
|
||||
# I should probably add a specific method for this or update create.
|
||||
|
||||
# Let's use a direct DB call here or add a method to service.
|
||||
# Adding a method to service is cleaner.
|
||||
|
||||
return EquivalencyItemService.create_nested(db, equivalency_id, data, tenant_id, company_id)
|
||||
|
||||
|
||||
router.include_router(equivalency_crud.router)
|
||||
router.include_router(item_crud.router)
|
||||
router.include_router(equivalency_crud.router)
|
||||
|
||||
@@ -7,130 +7,9 @@ from .models import Equivalency, EquivalencyItem
|
||||
from .dto import EquivalencyCreate, EquivalencyUpdate, EquivalencyItemCreate, EquivalencyItemUpdate
|
||||
|
||||
|
||||
class EquivalencyService:
|
||||
@staticmethod
|
||||
def get_all(
|
||||
db: Session,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
filters: Optional[Dict[str, Any]] = None
|
||||
) -> Tuple[List[Equivalency], int]:
|
||||
query = db.query(Equivalency).filter(
|
||||
Equivalency.tenant_id == tenant_id,
|
||||
Equivalency.company_id == company_id,
|
||||
)
|
||||
|
||||
if filters:
|
||||
# Soporta filtros de UI por nombre de parámetro.
|
||||
if "from_unit_code" in filters:
|
||||
query = query.filter(
|
||||
Equivalency.identifier.ilike(f"%{filters['from_unit_code']}%")
|
||||
)
|
||||
if "identifier" in filters:
|
||||
query = query.filter(
|
||||
Equivalency.identifier.ilike(f"%{filters['identifier']}%")
|
||||
)
|
||||
if "description" in filters:
|
||||
query = query.filter(
|
||||
Equivalency.description.ilike(f"%{filters['description']}%")
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
items = query.offset(skip).limit(limit).all()
|
||||
return items, total
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(
|
||||
db: Session,
|
||||
id: int,
|
||||
tenant_id: int,
|
||||
company_id: int
|
||||
) -> Optional[Equivalency]:
|
||||
item = db.query(Equivalency).filter(
|
||||
Equivalency.id == id,
|
||||
Equivalency.tenant_id == tenant_id,
|
||||
Equivalency.company_id == company_id
|
||||
).first()
|
||||
|
||||
return item
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
db: Session,
|
||||
data: EquivalencyCreate,
|
||||
tenant_id: int,
|
||||
company_id: int
|
||||
) -> Equivalency:
|
||||
try:
|
||||
db_obj = Equivalency(
|
||||
identifier=data.identifier,
|
||||
description=data.description,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
)
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
error_msg = str(e.orig) if hasattr(e, "orig") else str(e)
|
||||
if "uq_equivalency_identifier" in error_msg:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Ya existe un registro para el origen '{data.identifier}'.",
|
||||
) from e
|
||||
raise HTTPException(status_code=400, detail=f"Error al guardar: {error_msg}") from e
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def update(
|
||||
db: Session,
|
||||
id: int,
|
||||
tenant_id: int,
|
||||
data: EquivalencyUpdate,
|
||||
company_id: int
|
||||
) -> Optional[Equivalency]:
|
||||
db_obj = EquivalencyService.get_by_id(db, id, tenant_id, company_id)
|
||||
if not db_obj:
|
||||
return None
|
||||
|
||||
try:
|
||||
update_dict = data.model_dump(exclude_unset=True)
|
||||
for key, value in update_dict.items():
|
||||
setattr(db_obj, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
error_msg = str(e.orig) if hasattr(e, "orig") else str(e)
|
||||
raise HTTPException(status_code=400, detail=f"Error al actualizar: {error_msg}") from e
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def delete(
|
||||
db: Session,
|
||||
id: int,
|
||||
tenant_id: int,
|
||||
company_id: int
|
||||
) -> bool:
|
||||
db_obj = EquivalencyService.get_by_id(db, id, tenant_id, company_id)
|
||||
if not db_obj:
|
||||
return False
|
||||
|
||||
db.delete(db_obj)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
class EquivalencyItemService:
|
||||
"""CRUD simple sobre el pool global de items."""
|
||||
|
||||
@staticmethod
|
||||
def get_all(
|
||||
db: Session,
|
||||
@@ -146,15 +25,13 @@ class EquivalencyItemService:
|
||||
)
|
||||
|
||||
if filters:
|
||||
if "from_unit_code" in filters:
|
||||
if "original_field" in filters:
|
||||
query = query.filter(
|
||||
EquivalencyItem.original_field.ilike(
|
||||
f"%{filters['from_unit_code']}%"
|
||||
)
|
||||
EquivalencyItem.original_field.ilike(f"%{filters['original_field']}%")
|
||||
)
|
||||
if "to_unit_code" in filters:
|
||||
if "external_field" in filters:
|
||||
query = query.filter(
|
||||
EquivalencyItem.external_field.ilike(f"%{filters['to_unit_code']}%")
|
||||
EquivalencyItem.external_field.ilike(f"%{filters['external_field']}%")
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
@@ -182,29 +59,7 @@ class EquivalencyItemService:
|
||||
company_id: int
|
||||
) -> EquivalencyItem:
|
||||
try:
|
||||
# Equivalency padre agrupado por `from_unit_code` (original_field)
|
||||
parent = (
|
||||
db.query(Equivalency)
|
||||
.filter(
|
||||
Equivalency.tenant_id == tenant_id,
|
||||
Equivalency.company_id == company_id,
|
||||
Equivalency.identifier == data.original_field,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not parent:
|
||||
parent = Equivalency(
|
||||
identifier=data.original_field,
|
||||
description=None,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
)
|
||||
db.add(parent)
|
||||
db.flush() # obtener id sin commit
|
||||
|
||||
db_obj = EquivalencyItem(
|
||||
equivalency_id=parent.id,
|
||||
original_field=data.original_field,
|
||||
external_field=data.external_field,
|
||||
conversion_factor=data.conversion_factor,
|
||||
@@ -218,36 +73,13 @@ class EquivalencyItemService:
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
error_msg = str(e.orig) if hasattr(e, "orig") else str(e)
|
||||
if "units_of_measure" in error_msg:
|
||||
if "uq_equivalency_item_fields" in error_msg:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=(
|
||||
"Verifica que las unidades existan en el catálogo de U.M."
|
||||
),
|
||||
detail=f"Ya existe un item con los mismos campos '{data.original_field}' → '{data.external_field}'."
|
||||
) from e
|
||||
raise HTTPException(status_code=400, detail=f"Error al guardar: {error_msg}") from e
|
||||
|
||||
@staticmethod
|
||||
def create_nested(
|
||||
db: Session,
|
||||
equivalency_id: int,
|
||||
data: EquivalencyItemCreate,
|
||||
tenant_id: int,
|
||||
company_id: int
|
||||
) -> EquivalencyItem:
|
||||
db_obj = EquivalencyItem(
|
||||
equivalency_id=equivalency_id,
|
||||
original_field=data.original_field,
|
||||
external_field=data.external_field,
|
||||
conversion_factor=data.conversion_factor,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
)
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
@staticmethod
|
||||
def update(
|
||||
db: Session,
|
||||
@@ -260,39 +92,10 @@ class EquivalencyItemService:
|
||||
if not db_obj:
|
||||
return None
|
||||
|
||||
update_dict = data.model_dump(exclude_unset=True)
|
||||
try:
|
||||
new_original = update_dict.get("original_field")
|
||||
if new_original and new_original != db_obj.original_field:
|
||||
# Re-asignar al padre del nuevo `from_unit_code`
|
||||
parent = (
|
||||
db.query(Equivalency)
|
||||
.filter(
|
||||
Equivalency.tenant_id == tenant_id,
|
||||
Equivalency.company_id == company_id,
|
||||
Equivalency.identifier == new_original,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not parent:
|
||||
parent = Equivalency(
|
||||
identifier=new_original,
|
||||
description=None,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
)
|
||||
db.add(parent)
|
||||
db.flush()
|
||||
|
||||
db_obj.equivalency_id = parent.id
|
||||
db_obj.original_field = new_original
|
||||
|
||||
# Aplicar resto de cambios
|
||||
update_dict = data.model_dump(exclude_unset=True)
|
||||
for key, value in update_dict.items():
|
||||
if key == "original_field":
|
||||
continue # ya aplicado arriba (y con posible re-parenting)
|
||||
setattr(db_obj, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
@@ -314,3 +117,119 @@ class EquivalencyItemService:
|
||||
db.delete(db_obj)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
|
||||
class EquivalencyService:
|
||||
"""CRUD del catálogo de equivalencias. Cada registro referencia un EquivalencyItem."""
|
||||
|
||||
@staticmethod
|
||||
def get_all(
|
||||
db: Session,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
filters: Optional[Dict[str, Any]] = None
|
||||
) -> Tuple[List[Equivalency], int]:
|
||||
query = db.query(Equivalency).filter(
|
||||
Equivalency.tenant_id == tenant_id,
|
||||
Equivalency.company_id == company_id,
|
||||
)
|
||||
|
||||
if filters:
|
||||
if "from_unit_code" in filters:
|
||||
query = query.filter(
|
||||
Equivalency.identifier.ilike(f"%{filters['from_unit_code']}%")
|
||||
)
|
||||
if "identifier" in filters:
|
||||
query = query.filter(
|
||||
Equivalency.identifier.ilike(f"%{filters['identifier']}%")
|
||||
)
|
||||
if "description" in filters:
|
||||
query = query.filter(
|
||||
Equivalency.description.ilike(f"%{filters['description']}%")
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
items = query.offset(skip).limit(limit).all()
|
||||
return items, total
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(
|
||||
db: Session,
|
||||
id: int,
|
||||
tenant_id: int,
|
||||
company_id: int
|
||||
) -> Optional[Equivalency]:
|
||||
return db.query(Equivalency).filter(
|
||||
Equivalency.id == id,
|
||||
Equivalency.tenant_id == tenant_id,
|
||||
Equivalency.company_id == company_id
|
||||
).first()
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
db: Session,
|
||||
data: EquivalencyCreate,
|
||||
tenant_id: int,
|
||||
company_id: int
|
||||
) -> Equivalency:
|
||||
try:
|
||||
db_obj = Equivalency(
|
||||
identifier=data.identifier,
|
||||
description=data.description,
|
||||
item_id=data.item_id,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
)
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
error_msg = str(e.orig) if hasattr(e, "orig") else str(e)
|
||||
if "uq_equivalency_identifier" in error_msg:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Ya existe un registro para el identificador '{data.identifier}'.",
|
||||
) from e
|
||||
raise HTTPException(status_code=400, detail=f"Error al guardar: {error_msg}") from e
|
||||
|
||||
@staticmethod
|
||||
def update(
|
||||
db: Session,
|
||||
id: int,
|
||||
tenant_id: int,
|
||||
data: EquivalencyUpdate,
|
||||
company_id: int
|
||||
) -> Optional[Equivalency]:
|
||||
db_obj = EquivalencyService.get_by_id(db, id, tenant_id, company_id)
|
||||
if not db_obj:
|
||||
return None
|
||||
|
||||
try:
|
||||
update_dict = data.model_dump(exclude_unset=True)
|
||||
for key, value in update_dict.items():
|
||||
setattr(db_obj, key, value)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
error_msg = str(e.orig) if hasattr(e, "orig") else str(e)
|
||||
raise HTTPException(status_code=400, detail=f"Error al actualizar: {error_msg}") from e
|
||||
|
||||
@staticmethod
|
||||
def delete(
|
||||
db: Session,
|
||||
id: int,
|
||||
tenant_id: int,
|
||||
company_id: int
|
||||
) -> bool:
|
||||
db_obj = EquivalencyService.get_by_id(db, id, tenant_id, company_id)
|
||||
if not db_obj:
|
||||
return False
|
||||
db.delete(db_obj)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
@@ -14,6 +14,7 @@ from core.database import Base
|
||||
|
||||
from api.v1.modules.a76.classes.models import Class
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure
|
||||
from api.v1.modules.a76.general_catalogs.identifiers.models import IdentifierDetail
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .line_financials.models import LineFinancial
|
||||
@@ -24,7 +25,6 @@ if TYPE_CHECKING:
|
||||
from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem
|
||||
from api.v1.modules.a76.parts.models import Part
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
from api.v1.modules.a76.general_catalogs.identifiers.models import IdentifierDetail
|
||||
|
||||
# ============================================================================
|
||||
# CORE ENTITIES
|
||||
@@ -221,7 +221,7 @@ class LineItem(Base, TenantScopedMixin, TimestampMixin):
|
||||
uselist=False,
|
||||
)
|
||||
identifiers: Mapped[List["IdentifierDetail"]] = relationship(
|
||||
"IdentifierDetail",
|
||||
IdentifierDetail,
|
||||
back_populates="line",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user