feature/continuacion-vista-tablas-location

This commit is contained in:
hreyes
2026-03-12 16:14:34 -06:00
parent 678f91ecdc
commit 8d304fb98b
23 changed files with 1337 additions and 717 deletions

View File

@@ -0,0 +1 @@
# a76 general_catalogs.location

View File

@@ -0,0 +1,36 @@
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
from .models import LocationSystem
class LocationBase(BaseModel):
clave_localizacion: str = Field(
..., max_length=20, description="Clave/código de la localización"
)
localizacion: Optional[str] = Field(
None, max_length=200, description="Nombre o descripción"
)
system: LocationSystem = Field(
..., description="Contexto: fixed_asset (FA) o inventory"
)
class LocationCreate(LocationBase):
"""Optional extra fields for fixed_asset; used only when system == FIXED_ASSET."""
department: Optional[str] = Field(None, max_length=100)
responsible: Optional[str] = Field(None, max_length=200)
observations: Optional[str] = Field(None, description="Free text")
class LocationUpdate(BaseModel):
clave_localizacion: Optional[str] = Field(None, max_length=20)
localizacion: Optional[str] = Field(None, max_length=200)
system: Optional[LocationSystem] = None
class LocationResponse(LocationBase):
id: int
model_config = ConfigDict(from_attributes=True)

View File

@@ -0,0 +1,84 @@
"""
Modelo ORM para catálogo de localización (a76).
Tabla compartida para contexto Fixed Asset (FA) e inventory.
"""
import enum
from typing import Optional
from sqlalchemy import Integer, String, Text, UniqueConstraint, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
class LocationSystem(str, enum.Enum):
"""Contexto de uso de la localización."""
FIXED_ASSET = "fixed_asset"
INVENTORY = "inventory"
class Location(Base, TenantScopedMixin, TimestampMixin):
"""
Catálogo de localización (clave + localizacion), compartido por FA e inventory.
"""
__tablename__ = "location"
__table_args__ = (
UniqueConstraint(
"clave_localizacion",
"tenant_id",
"company_id",
"system",
name="uq_location_clave_tenant_company_system",
),
{"schema": "a76"},
)
id: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True
)
clave_localizacion: Mapped[str] = mapped_column(String(20), nullable=False)
localizacion: Mapped[Optional[str]] = mapped_column(String(200), nullable=True)
system: Mapped[str] = mapped_column(
String(20), nullable=False
) # 'fixed_asset' | 'inventory'
def __repr__(self) -> str:
return (
f"<Location(id={self.id}, clave_localizacion={self.clave_localizacion!r}, "
f"localizacion={self.localizacion!r}, system={self.system!r})>"
)
class FaLocationExt(Base, TenantScopedMixin, TimestampMixin):
"""
Extra info for Fixed Asset locations only (1:1 with Location).
Table: Fa_Location_Ext (fa_location_ext).
"""
__tablename__ = "fa_location_ext"
__table_args__ = (
UniqueConstraint("location_id", name="uq_fa_location_ext_location_id"),
{"schema": "a76"},
)
id: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True
)
location_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("a76.location.id", ondelete="CASCADE"),
nullable=False,
unique=True,
)
department: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
responsible: Mapped[Optional[str]] = mapped_column(String(200), nullable=True)
observations: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
def __repr__(self) -> str:
return (
f"<FaLocationExt(id={self.id}, location_id={self.location_id}, "
f"department={self.department!r})>"
)

View File

@@ -0,0 +1,20 @@
from fastapi import APIRouter
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from .dto import LocationCreate, LocationResponse, LocationUpdate
from .models import Location
from .service import LocationService
router = TenantCRUDRoutes(
service=LocationService,
create_schema=LocationCreate,
update_schema=LocationUpdate,
response_schema=LocationResponse,
prefix="/locations",
tags=["a76.general_catalogs.locations"],
resource_name="Location",
enable_list=True,
enable_filters=True,
max_page_size=1000,
).router

View File

@@ -0,0 +1,154 @@
from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from .models import Location, FaLocationExt
from .dto import LocationCreate, LocationUpdate
class LocationService:
@staticmethod
def get_all(
db: Session,
tenant_id: int,
company_id: Optional[int],
skip: int = 0,
limit: int = 100,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[Location], int]:
query = db.query(Location).filter(Location.tenant_id == tenant_id)
if company_id is not None:
query = query.filter(Location.company_id == company_id)
if filters:
if filters.get("clave_localizacion"):
query = query.filter(
Location.clave_localizacion.ilike(
f"%{filters['clave_localizacion']}%"
)
)
if filters.get("localizacion"):
query = query.filter(
Location.localizacion.ilike(f"%{filters['localizacion']}%")
)
if filters.get("system"):
query = query.filter(Location.system == filters["system"])
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[Location]:
return (
db.query(Location)
.filter(
Location.id == id,
Location.tenant_id == tenant_id,
Location.company_id == company_id,
)
.first()
)
@staticmethod
def get_by_clave(
db: Session,
clave_localizacion: str,
tenant_id: int,
company_id: int,
system: Optional[str] = None,
) -> Optional[Location]:
query = db.query(Location).filter(
Location.clave_localizacion == clave_localizacion,
Location.tenant_id == tenant_id,
Location.company_id == company_id,
)
if system is not None:
query = query.filter(Location.system == system)
return query.first()
@staticmethod
def create(
db: Session,
data: LocationCreate,
tenant_id: int,
company_id: int,
) -> Location:
db_obj = Location(
clave_localizacion=data.clave_localizacion,
localizacion=data.localizacion,
system=data.system.value,
tenant_id=tenant_id,
company_id=company_id,
)
db.add(db_obj)
try:
db.flush() # get db_obj.id without committing
if data.system.value == "fixed_asset" and (
data.department is not None
or data.responsible is not None
or data.observations is not None
):
ext = FaLocationExt(
location_id=db_obj.id,
department=data.department,
responsible=data.responsible,
observations=data.observations,
tenant_id=tenant_id,
company_id=company_id,
)
db.add(ext)
db.commit()
db.refresh(db_obj)
return db_obj
except IntegrityError:
db.rollback()
raise ValueError("Ya existe una localización con esa clave y system")
@staticmethod
def update(
db: Session,
id: int,
tenant_id: int,
data: LocationUpdate,
company_id: int,
) -> Optional[Location]:
db_obj = LocationService.get_by_id(db, id, tenant_id, company_id)
if not db_obj:
return None
update_dict = data.model_dump(exclude_unset=True)
for key, value in update_dict.items():
if key == "system" and value is not None:
setattr(db_obj, key, value.value)
else:
setattr(db_obj, key, value)
try:
db.commit()
db.refresh(db_obj)
return db_obj
except IntegrityError:
db.rollback()
raise ValueError("Ya existe una localización con esa clave y system")
@staticmethod
def delete(
db: Session,
id: int,
tenant_id: int,
company_id: int,
) -> bool:
db_obj = LocationService.get_by_id(db, id, tenant_id, company_id)
if not db_obj:
return False
db.delete(db_obj)
db.commit()
return True

View File

@@ -25,12 +25,14 @@ from .error_catalogs.routes import router as error_catalogs_router
from .doda.routes import router as doda_router
from .prevalidators.routes import router as prevalidators_router
from .electronic_notices.routes import router as electronic_notices_router
from .location.routes import router as location_router
router = APIRouter()
router.include_router(company_router, tags=["a76 / company"])
router.include_router(package_router)
router.include_router(ports_router)
router.include_router(location_router)
router.include_router(tariff_fractions_router)
router.include_router(us_tariff_fractions_router)
router.include_router(historical_tariff_fractions_router, prefix="/fractions/historical-tariff-fractions", tags=["a76 / historical_tariff_fractions"])