feature/refactorizacion-tabla-sector

This commit is contained in:
hreyes
2026-03-17 11:21:28 -06:00
parent 20a351c475
commit fbbe4370de
32 changed files with 4167 additions and 3947 deletions

View File

@@ -16,6 +16,7 @@ from ...audit_log.services.service import AuditService
from ..units_of_measure.seed import seed as units_of_measure_seed
from ..fractions.historical_tariff_fractions.seed import seed as historical_tariff_fractions_seed
from ..fractions.warning_fractions.seed import seed as warning_fractions_seed
from ..sectors.seed import seed as sectors_seed
from core.context import get_user_context
from sqlalchemy import text
@@ -774,6 +775,21 @@ class CompanyService:
"""))
db.execute(text("ALTER TABLE public.warning_fractions ENABLE TRIGGER ALL;"))
# 4. Sectors
values_sectors = ", ".join(
[
f"({format_value(key)}, {format_value(description)}, {str(authorized).upper()}, {tenant_id}, {company_id})"
for key, description, authorized in sectors_seed
]
)
if values_sectors:
db.execute(text(f"""
INSERT INTO a76.sectors (key, description, authorized, tenant_id, company_id)
VALUES {values_sectors}
ON CONFLICT (key, tenant_id, company_id) DO NOTHING;
"""))
def get_companies_by_tenant(self, tenant_id: int) -> List[Company]:
"""Get all companies for a tenant"""
return (

View File

@@ -26,6 +26,7 @@ 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
from .sectors.routes import router as sectors_router
router = APIRouter()
@@ -56,3 +57,4 @@ router.include_router(error_catalogs_router)
router.include_router(doda_router)
router.include_router(prevalidators_router)
router.include_router(electronic_notices_router)
router.include_router(sectors_router)

View File

@@ -0,0 +1,30 @@
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
class SectorBaseDTO(BaseModel):
key: str = Field(..., description="Clave del sector (ej: 'XIX', 'IIa')", max_length=8)
description: str = Field(..., description="Descripción del sector", max_length=150)
authorized: Optional[bool] = Field(False, description="True = autorizado para PROSEC")
class SectorCreateDTO(SectorBaseDTO):
pass
class SectorUpdateDTO(BaseModel):
key: Optional[str] = Field(None, max_length=8)
description: Optional[str] = Field(None, max_length=150)
authorized: Optional[bool] = None
class SectorResponseDTO(SectorBaseDTO):
id: int
company_id: int
tenant_id: int
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True)

View File

@@ -0,0 +1,23 @@
from typing import Optional
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import Boolean, Integer, PrimaryKeyConstraint, String, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
class Sector(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "sectors" # GSectores
__table_args__ = (
PrimaryKeyConstraint("id", name="sectors_pkey"),
UniqueConstraint("tenant_id", "company_id", "key", name="sectors_key_ukey"),
{"schema": "a76", "extend_existing": True},
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
key: Mapped[str] = mapped_column(String(8), nullable=False)
description: Mapped[str] = mapped_column(String(150), nullable=False)
authorized: Mapped[Optional[bool]] = mapped_column(Boolean, default=False, server_default="false")
def __repr__(self):
return f"<Sector(id={self.id}, key={self.key}, description={self.description}, authorized={self.authorized})>"

View File

@@ -0,0 +1,23 @@
"""
Routes for managing Sectors (GSectores) — a76 tenant-scoped catalog.
"""
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from .dto import SectorCreateDTO, SectorResponseDTO, SectorUpdateDTO
from .service import SectorService
router = TenantCRUDRoutes(
service=SectorService,
create_schema=SectorCreateDTO,
update_schema=SectorUpdateDTO,
response_schema=SectorResponseDTO,
prefix="/sectors",
tags=["a76 / sectors"],
resource_name="Sector",
id_name="sector_id",
enable_list=True,
enable_filters=True,
default_page_size=50,
max_page_size=100,
).router

View File

@@ -0,0 +1,36 @@
# (key, description, authorized)
seed = [
("I", "INDUSTRIA ELECTRICA", False),
("II", "INDUSTRIA ELECTRONICA", False),
("IIa", "PARA LOS BIENES A QUE SE REFIERE LA FRACCION II, INCISO a) o b), DE ARTICULO 4to DE ESTE DECRETO.", False),
("IIb", "PARA LOS BIENES A QUE SE REFIERE LA FRACCION II, INCISO b), DE ARTICULO 4to DE ESTE DECRETO.", False),
("III", "INDUSTRIA DEL MUEBLE", False),
("IV", "INDUSTRIA DEL JUGUETE, JUEGOS DE RECREO Y ARTICULOS DEPORTIVOS", False),
("IX", "INDUSTRIA DE MAQUINARIA AGRICOLA", False),
("V", "INDUSTRIA DEL CALZADO", False),
("VI", "INDUSTRIA MINERA Y METALURGICA", False),
("VII", "INDUSTRIA DE BIENES DE CAPITAL", False),
("VIII", "INDUSTRIA FOTOGRAFICA", False),
("X", "INDUSTRIAS DIVERSAS", False),
("XI", "INDUSTRIA QUIMICA", False),
("XII", "INDUSTRIAS DE MANUFACTURAS DEL CAUCHO Y PLASTICOS", False),
("XIII", "INDUSTRIA SIDERURGICA", False),
("XIV", "INDUSTRIA DE PRODUCTOS FARMOQUIMICOS, MEDICAMENTOS Y EQUIPO MEDICO", False),
("XIX", "INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES", False),
("XIXa", "INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES", False),
("XIXb", "INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES", False),
("XV", "INDUSTRIA DEL TRANSPORTE, EXCEPTO EL SECTOR DE LA INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES", False),
("XVa", "INDUSTRIA DEL TRANSPORTE, EXCEPTO EL SECTOR DE LA INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES.", False),
("XVb", "INDUSTRIA DEL TRANSPORTE, EXCEPTO EL SECTOR DE LA INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES.", False),
("XVI", "INDUSTRIA DEL PAPEL Y CARTON", False),
("XVII", "INDUSTRIA DE LA MADERA", False),
("XVIII", "INDUSTRIA DEL CUERO Y PIELES", False),
("XX", "INDUSTRIA TEXTIL Y DE LA CONFECCION", False),
("XXa", "INDUSTRIA TEXTIL Y DE LA CONFECCION", False),
("XXb", "INDUSTRIA TEXTIL Y DE LA CONFECCION", False),
("XXc", "INDUSTRIA TEXTIL Y DE LA CONFECCION", False),
("XXd", "INDUSTRIA TEXTIL Y DE LA CONFECCION", False),
("XXe", "INDUSTRIA TEXTIL Y DE LA CONFECCION", False),
("XXI", "INDUSTRIA DE CHOCOLATES, DULCES Y SIMILARES", False),
("XXII", "INDUSTRIA DEL CAFE", False),
]

View File

@@ -0,0 +1,143 @@
"""
Service layer for Sectors (GSectores) — a76 tenant-scoped catalog.
"""
import logging
from typing import Any, Dict, List, Optional, Tuple
from fastapi import HTTPException
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from . import dto, models
logger = logging.getLogger(__name__)
class SectorService:
"""Service for Sector CRUD operations with tenant support"""
@staticmethod
def get_all(
db: Session,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 50,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[models.Sector], int]:
"""Get all sectors for a tenant/company with pagination"""
query = db.query(models.Sector).filter(
models.Sector.tenant_id == tenant_id,
models.Sector.company_id == company_id,
)
if filters:
if filters.get("key"):
query = query.filter(
models.Sector.key.ilike(f"%{filters['key']}%")
)
if filters.get("description"):
query = query.filter(
models.Sector.description.ilike(f"%{filters['description']}%")
)
total = query.count()
sectors = query.order_by(models.Sector.key).offset(skip).limit(limit).all()
return sectors, total
@staticmethod
def get_by_id(
db: Session, sector_id: int, tenant_id: int, company_id: int
) -> Optional[models.Sector]:
"""Get sector by ID"""
return (
db.query(models.Sector)
.filter(
models.Sector.id == sector_id,
models.Sector.tenant_id == tenant_id,
models.Sector.company_id == company_id,
)
.first()
)
@staticmethod
def create(
db: Session,
sector_data: dto.SectorCreateDTO,
tenant_id: int,
company_id: int,
) -> models.Sector:
"""Create a new sector"""
new_sector = models.Sector(
**sector_data.model_dump(), tenant_id=tenant_id, company_id=company_id
)
db.add(new_sector)
try:
db.commit()
db.refresh(new_sector)
return new_sector
except IntegrityError as e:
db.rollback()
logger.error(f"IntegrityError creating sector: {str(e)}")
raise HTTPException(
status_code=400,
detail="Ya existe un sector con esa clave para esta empresa.",
)
@staticmethod
def update(
db: Session,
sector_id: int,
tenant_id: int,
sector_data: dto.SectorUpdateDTO,
company_id: int,
) -> Optional[models.Sector]:
"""Update a sector"""
sector = SectorService.get_by_id(db, sector_id, tenant_id, company_id)
if not sector:
return None
update_data = sector_data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(sector, field, value)
try:
db.commit()
db.refresh(sector)
return sector
except IntegrityError as e:
db.rollback()
logger.error(f"IntegrityError updating sector {sector_id}: {str(e)}")
raise HTTPException(
status_code=400,
detail="Ya existe un sector con esa clave para esta empresa.",
)
@staticmethod
def delete(
db: Session, sector_id: int, tenant_id: int, company_id: int
) -> bool:
"""Delete a sector"""
sector = SectorService.get_by_id(db, sector_id, tenant_id, company_id)
if not sector:
return False
try:
db.delete(sector)
db.commit()
return True
except IntegrityError as e:
db.rollback()
logger.error(f"IntegrityError deleting sector {sector_id}: {str(e)}")
if "foreign key constraint" in str(e).lower():
raise HTTPException(
status_code=400,
detail="No se puede eliminar el sector porque tiene registros relacionados.",
)
raise HTTPException(status_code=400, detail="Error al eliminar el sector.")
except Exception as e:
db.rollback()
logger.error(f"Error deleting sector {sector_id}: {str(e)}")
raise HTTPException(status_code=500, detail="Error al eliminar el sector.")