Merge pull request 'feature/general_catalogs_frontend' (#24) from feature/general_catalogs_frontend into development

Reviewed-on: ADUANASOFT/anexo76#24
This commit is contained in:
2025-12-09 17:53:08 +00:00
211 changed files with 9340 additions and 2926 deletions

View File

@@ -20,7 +20,7 @@ class TimestampMixin:
class TenantScopedMixin:
"""Mixin for tenant and company scoped entities"""
tenant_id: Mapped[int] = mapped_column(Integer, ForeignKey("a76.tenants.id"), nullable=False, index=True)
tenant_id: Mapped[int] = mapped_column(Integer, ForeignKey("core.tenants.id"), nullable=False, index=True)
company_id: Mapped[int] = mapped_column(Integer, ForeignKey("a76.company.id"), nullable=False, index=True)

View File

@@ -39,6 +39,11 @@ class Class(Base, TenantScopedMixin, TimestampMixin):
["public.material_types.key"],
name="fk_classes_material_type",
),
ForeignKeyConstraint(
["unit_of_measure", "tenant_id", "company_id"],
["a76.units_of_measure.code", "a76.units_of_measure.tenant_id",
"a76.units_of_measure.company_id"],
),
UniqueConstraint(
"tenant_id",
"company_id",
@@ -63,10 +68,10 @@ class Class(Base, TenantScopedMixin, TimestampMixin):
# Material and measurement
material_key: Mapped[Optional[str]] = mapped_column(
String(10), ForeignKey("public.material_types.key")
String(10)
) # CLAVEMAT - homologated from TIPOMAT/TIPOMATEQUIPO
unit_of_measure: Mapped[Optional[str]] = mapped_column(
String(5), ForeignKey("a76.units_of_measure.code")
String(5)
) # UNIMED - homologated from UNIMEDIDA
# Tariff fractions

View File

@@ -9,7 +9,7 @@ class ClassificationConcept(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "classification_concepts"
__table_args__ = (
UniqueConstraint("classification", name="uq_classification_concept"),
{"schema": "a76"}
{"schema": "a76", "extend_existing": True},
)
id: Mapped[int] = mapped_column(

View File

@@ -1,64 +1,14 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from core.database import get_core_db
from . import service
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from .dto import ClassificationConceptCreate, ClassificationConceptResponse, ClassificationConceptUpdate
from .service import ClassificationConceptService
router = APIRouter(prefix="/classification-concepts",
tags=["a76.general_catalogs.classification_concepts"])
@router.post("/", response_model=ClassificationConceptResponse, status_code=status.HTTP_201_CREATED)
def create_classification_concept(
data: ClassificationConceptCreate,
session: Session = Depends(get_core_db)
):
return service.create_classification_concept(session, data)
@router.get("/{id}", response_model=ClassificationConceptResponse)
def get_classification_concept(
id: int,
session: Session = Depends(get_core_db)
):
db_obj = service.get_classification_concept(session, id)
if not db_obj:
raise HTTPException(
status_code=404, detail="ClassificationConcept not found")
return db_obj
@router.get("/", response_model=List[ClassificationConceptResponse])
def get_classification_concepts(
skip: int = 0,
limit: int = 100,
session: Session = Depends(get_core_db)
):
return service.get_classification_concepts(session, skip, limit)
@router.put("/{id}", response_model=ClassificationConceptResponse)
def update_classification_concept(
id: int,
data: ClassificationConceptUpdate,
session: Session = Depends(get_core_db)
):
db_obj = service.get_classification_concept(session, id)
if not db_obj:
raise HTTPException(
status_code=404, detail="ClassificationConcept not found")
return service.update_classification_concept(session, db_obj, data)
@router.delete("/{id}", response_model=ClassificationConceptResponse)
def delete_classification_concept(
id: int,
session: Session = Depends(get_core_db)
):
db_obj = service.get_classification_concept(session, id)
if not db_obj:
raise HTTPException(
status_code=404, detail="ClassificationConcept not found")
return service.delete_classification_concept(session, db_obj)
router = TenantCRUDRoutes(
service=ClassificationConceptService,
create_schema=ClassificationConceptCreate,
update_schema=ClassificationConceptUpdate,
response_schema=ClassificationConceptResponse,
prefix="/classification-concepts",
tags=["a76.general_catalogs.classification_concepts"],
resource_name="Classification Concept",
enable_list=True,
).router

View File

@@ -1,39 +1,81 @@
from typing import List, Optional, Tuple, Dict, Any
from sqlalchemy.orm import Session
from sqlalchemy import select
from typing import Sequence, Optional
from .models import ClassificationConcept
from .dto import ClassificationConceptCreate, ClassificationConceptUpdate
def create_classification_concept(session: Session, data: ClassificationConceptCreate) -> ClassificationConcept:
db_obj = ClassificationConcept(**data.model_dump())
session.add(db_obj)
session.commit()
session.refresh(db_obj)
return db_obj
class ClassificationConceptService:
@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[ClassificationConcept], int]:
query = db.query(ClassificationConcept).filter(
ClassificationConcept.tenant_id == tenant_id,
ClassificationConcept.company_id == company_id
)
total = query.count()
items = query.offset(skip).limit(limit).all()
def get_classification_concept(session: Session, id: int) -> Optional[ClassificationConcept]:
return session.get(ClassificationConcept, id)
return items, total
@staticmethod
def get_by_id(
db: Session, id: int, tenant_id: int, company_id: int
) -> Optional[ClassificationConcept]:
return db.query(ClassificationConcept).filter(
ClassificationConcept.id == id,
ClassificationConcept.tenant_id == tenant_id,
ClassificationConcept.company_id == company_id
).first()
def get_classification_concepts(session: Session, skip: int = 0, limit: int = 100) -> Sequence[ClassificationConcept]:
stmt = select(ClassificationConcept).offset(skip).limit(limit)
result = session.execute(stmt)
return result.scalars().all()
@staticmethod
def create(
db: Session, data: ClassificationConceptCreate, tenant_id: int, company_id: int
) -> ClassificationConcept:
db_obj = ClassificationConcept(
**data.model_dump(),
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, id: int, tenant_id: int, data: ClassificationConceptUpdate, company_id: int
) -> Optional[ClassificationConcept]:
db_obj = ClassificationConceptService.get_by_id(
db, id, tenant_id, company_id)
if not db_obj:
return None
def update_classification_concept(session: Session, db_obj: ClassificationConcept, update_data: ClassificationConceptUpdate) -> ClassificationConcept:
update_dict = update_data.model_dump(exclude_unset=True)
for key, value in update_dict.items():
setattr(db_obj, key, value)
session.commit()
session.refresh(db_obj)
return db_obj
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
def delete_classification_concept(session: Session, db_obj: ClassificationConcept) -> ClassificationConcept:
session.delete(db_obj)
session.commit()
return db_obj
@staticmethod
def delete(
db: Session, id: int, tenant_id: int, company_id: int
) -> bool:
db_obj = ClassificationConceptService.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

@@ -31,7 +31,7 @@ class Company(Base, TimestampMixin):
# Primary key
id: Mapped[int] = mapped_column(Integer, primary_key=True)
tenant_id: Mapped[int] = mapped_column(Integer, ForeignKey("a76.tenants.id"), nullable=False, index=True)
tenant_id: Mapped[int] = mapped_column(Integer, ForeignKey("core.tenants.id"), nullable=False, index=True)
# Información básica de la empresa
name: Mapped[Optional[str]] = mapped_column(String(255))

View File

@@ -3,7 +3,10 @@ from sqlalchemy import Integer, String, UniqueConstraint, Boolean, ForeignKey
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.classification_concepts.models import ClassificationConcept
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from api.v1.modules.a76.general_catalogs.classification_concepts.models import ClassificationConcept
class Concept(Base, TenantScopedMixin, TimestampMixin):

View File

@@ -1,71 +1,14 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from core.database import get_core_db
from core.security import get_current_user, validate_access_to_resource
from . import service
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from .dto import ConceptCreate, ConceptResponse, ConceptUpdate
from .service import ConceptService
router = APIRouter(prefix="/concepts", tags=["a76.general_catalogs.concepts"])
@router.post("/", response_model=ConceptResponse, status_code=status.HTTP_201_CREATED)
def create_concept(
data: ConceptCreate,
session: Session = Depends(get_core_db),
current_user=Depends(get_current_user)
):
tenant_id = validate_access_to_resource(
session, data.company_id, current_user)
return service.create_concept(session, data, tenant_id)
@router.get("/{id}", response_model=ConceptResponse)
def get_concept(
id: int,
session: Session = Depends(get_core_db),
current_user=Depends(get_current_user)
):
db_obj = service.get_concept(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="Concept not found")
validate_access_to_resource(session, db_obj.company_id, current_user)
return db_obj
@router.get("/", response_model=List[ConceptResponse])
def get_concepts(
skip: int = 0,
limit: int = 100,
session: Session = Depends(get_core_db),
current_user=Depends(get_current_user)
):
return service.get_concepts(session, skip, limit)
@router.put("/{id}", response_model=ConceptResponse)
def update_concept(
id: int,
data: ConceptUpdate,
session: Session = Depends(get_core_db),
current_user=Depends(get_current_user)
):
db_obj = service.get_concept(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="Concept not found")
validate_access_to_resource(session, db_obj.company_id, current_user)
return service.update_concept(session, db_obj, data)
@router.delete("/{id}", response_model=ConceptResponse)
def delete_concept(
id: int,
session: Session = Depends(get_core_db),
current_user=Depends(get_current_user)
):
db_obj = service.get_concept(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="Concept not found")
validate_access_to_resource(session, db_obj.company_id, current_user)
return service.delete_concept(session, db_obj)
router = TenantCRUDRoutes(
service=ConceptService,
create_schema=ConceptCreate,
update_schema=ConceptUpdate,
response_schema=ConceptResponse,
prefix="/concepts",
tags=["a76.general_catalogs.concepts"],
resource_name="Concept",
enable_list=True,
).router

View File

@@ -1,36 +1,79 @@
from typing import List, Optional, Tuple, Dict, Any
from sqlalchemy.orm import Session
from typing import Sequence, Optional
from sqlalchemy import select
from .models import Concept
from .dto import ConceptCreate, ConceptUpdate
def create_concept(session: Session, data: ConceptCreate, tenant_id: int) -> Concept:
db_obj = Concept(**data.model_dump(), tenant_id=tenant_id)
session.add(db_obj)
session.commit()
session.refresh(db_obj)
return db_obj
class ConceptService:
@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[Concept], int]:
query = db.query(Concept).filter(
Concept.tenant_id == tenant_id,
Concept.company_id == company_id
)
total = query.count()
items = query.offset(skip).limit(limit).all()
def get_concept(session: Session, id: int) -> Optional[Concept]:
return session.get(Concept, id)
return items, total
@staticmethod
def get_by_id(
db: Session, id: int, tenant_id: int, company_id: int
) -> Optional[Concept]:
return db.query(Concept).filter(
Concept.id == id,
Concept.tenant_id == tenant_id,
Concept.company_id == company_id
).first()
def get_concepts(session: Session, skip: int = 0, limit: int = 100) -> Sequence[Concept]:
return session.query(Concept).offset(skip).limit(limit).all()
@staticmethod
def create(
db: Session, data: ConceptCreate, tenant_id: int, company_id: int
) -> Concept:
data_dict = data.model_dump()
data_dict['company_id'] = company_id
data_dict['tenant_id'] = tenant_id
db_obj = Concept(**data_dict)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update_concept(session: Session, db_obj: Concept, update_data: ConceptUpdate) -> Concept:
update_dict = update_data.model_dump(exclude_unset=True)
for key, value in update_dict.items():
setattr(db_obj, key, value)
session.commit()
session.refresh(db_obj)
return db_obj
@staticmethod
def update(
db: Session, id: int, tenant_id: int, data: ConceptUpdate, company_id: int
) -> Optional[Concept]:
db_obj = ConceptService.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():
setattr(db_obj, key, value)
def delete_concept(session: Session, db_obj: Concept) -> Concept:
session.delete(db_obj)
session.commit()
return db_obj
db.commit()
db.refresh(db_obj)
return db_obj
@staticmethod
def delete(
db: Session, id: int, tenant_id: int, company_id: int
) -> bool:
db_obj = ConceptService.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

@@ -1,64 +1,14 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from core.database import get_core_db
from . import service
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from .dto import CustomsBrokerConceptCreate, CustomsBrokerConceptResponse, CustomsBrokerConceptUpdate
from .service import CustomsBrokerConceptService
router = APIRouter(prefix="/customs-broker-concepts",
tags=["a76.general_catalogs.customs_broker_concepts"])
@router.post("/", response_model=CustomsBrokerConceptResponse, status_code=status.HTTP_201_CREATED)
def create_customs_broker_concept(
data: CustomsBrokerConceptCreate,
session: Session = Depends(get_core_db)
):
return service.create_customs_broker_concept(session, data)
@router.get("/{id}", response_model=CustomsBrokerConceptResponse)
def get_customs_broker_concept(
id: int,
session: Session = Depends(get_core_db)
):
db_obj = service.get_customs_broker_concept(session, id)
if not db_obj:
raise HTTPException(
status_code=404, detail="CustomsBrokerConcept not found")
return db_obj
@router.get("/", response_model=List[CustomsBrokerConceptResponse])
def get_customs_broker_concepts(
skip: int = 0,
limit: int = 100,
session: Session = Depends(get_core_db)
):
return service.get_customs_broker_concepts(session, skip, limit)
@router.put("/{id}", response_model=CustomsBrokerConceptResponse)
def update_customs_broker_concept(
id: int,
data: CustomsBrokerConceptUpdate,
session: Session = Depends(get_core_db)
):
db_obj = service.get_customs_broker_concept(session, id)
if not db_obj:
raise HTTPException(
status_code=404, detail="CustomsBrokerConcept not found")
return service.update_customs_broker_concept(session, db_obj, data)
@router.delete("/{id}", response_model=CustomsBrokerConceptResponse)
def delete_customs_broker_concept(
id: int,
session: Session = Depends(get_core_db)
):
db_obj = service.get_customs_broker_concept(session, id)
if not db_obj:
raise HTTPException(
status_code=404, detail="CustomsBrokerConcept not found")
return service.delete_customs_broker_concept(session, db_obj)
router = TenantCRUDRoutes(
service=CustomsBrokerConceptService,
create_schema=CustomsBrokerConceptCreate,
update_schema=CustomsBrokerConceptUpdate,
response_schema=CustomsBrokerConceptResponse,
prefix="/customs-broker-concepts",
tags=["a76.general_catalogs.customs_broker_concepts"],
resource_name="Customs Broker Concept",
enable_list=True,
).router

View File

@@ -1,39 +1,81 @@
from typing import List, Optional, Tuple, Dict, Any
from sqlalchemy.orm import Session
from sqlalchemy import select
from typing import Sequence, Optional
from .models import CustomsBrokerConcept
from .dto import CustomsBrokerConceptCreate, CustomsBrokerConceptUpdate
def create_customs_broker_concept(session: Session, data: CustomsBrokerConceptCreate) -> CustomsBrokerConcept:
db_obj = CustomsBrokerConcept(**data.model_dump())
session.add(db_obj)
session.commit()
session.refresh(db_obj)
return db_obj
class CustomsBrokerConceptService:
@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[CustomsBrokerConcept], int]:
query = db.query(CustomsBrokerConcept).filter(
CustomsBrokerConcept.tenant_id == tenant_id,
CustomsBrokerConcept.company_id == company_id
)
total = query.count()
items = query.offset(skip).limit(limit).all()
def get_customs_broker_concept(session: Session, id: int) -> Optional[CustomsBrokerConcept]:
return session.get(CustomsBrokerConcept, id)
return items, total
@staticmethod
def get_by_id(
db: Session, id: int, tenant_id: int, company_id: int
) -> Optional[CustomsBrokerConcept]:
return db.query(CustomsBrokerConcept).filter(
CustomsBrokerConcept.id == id,
CustomsBrokerConcept.tenant_id == tenant_id,
CustomsBrokerConcept.company_id == company_id
).first()
def get_customs_broker_concepts(session: Session, skip: int = 0, limit: int = 100) -> Sequence[CustomsBrokerConcept]:
stmt = select(CustomsBrokerConcept).offset(skip).limit(limit)
result = session.execute(stmt)
return result.scalars().all()
@staticmethod
def create(
db: Session, data: CustomsBrokerConceptCreate, tenant_id: int, company_id: int
) -> CustomsBrokerConcept:
db_obj = CustomsBrokerConcept(
**data.model_dump(),
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, id: int, tenant_id: int, data: CustomsBrokerConceptUpdate, company_id: int
) -> Optional[CustomsBrokerConcept]:
db_obj = CustomsBrokerConceptService.get_by_id(
db, id, tenant_id, company_id)
if not db_obj:
return None
def update_customs_broker_concept(session: Session, db_obj: CustomsBrokerConcept, update_data: CustomsBrokerConceptUpdate) -> CustomsBrokerConcept:
update_dict = update_data.model_dump(exclude_unset=True)
for key, value in update_dict.items():
setattr(db_obj, key, value)
session.commit()
session.refresh(db_obj)
return db_obj
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
def delete_customs_broker_concept(session: Session, db_obj: CustomsBrokerConcept) -> CustomsBrokerConcept:
session.delete(db_obj)
session.commit()
return db_obj
@staticmethod
def delete(
db: Session, id: int, tenant_id: int, company_id: int
) -> bool:
db_obj = CustomsBrokerConceptService.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

@@ -8,6 +8,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from core.database import get_core_db
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from .dto import (
DodaCreateDTO,
DodaResponseDTO,
@@ -25,54 +26,40 @@ from .dto import (
)
from .models import Doda
from .service import DodaService
from core.security import get_current_user, validate_access_to_resource
router = APIRouter(prefix="/doda", tags=["doda"])
# Create CRUD router
crud_router = TenantCRUDRoutes(
service=DodaService,
create_schema=DodaCreateDTO,
update_schema=DodaUpdateDTO,
response_schema=DodaResponseDTO,
prefix="/doda",
tags=["doda"],
resource_name="DODA",
id_name="doda_id",
enable_list=True,
enable_filters=True,
).router
router = crud_router
# ============ MAIN DODA ENDPOINTS ============
@router.get(
"",
response_model=dict,
summary="Get all DODAs",
)
async def get_all_dodas(
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
integration_number: str = Query(None),
status: str = Query(None),
patent: str = Query(None),
db: Session = Depends(get_core_db),
):
"""Get all DODAs with optional filtering and pagination"""
filters = {}
if integration_number:
filters["integration_number"] = integration_number
if status:
filters["status"] = status
if patent:
filters["patent"] = patent
dodas, total = DodaService.get_all(db, skip, limit, filters)
return {
"data": [DodaResponseDTO.model_validate(doda) for doda in dodas],
"total": total,
"skip": skip,
"limit": limit,
}
# ============ CUSTOM ENDPOINTS ============
@router.get(
"/{sys_id}",
"/{doda_id}/detail",
response_model=DodaDetailResponseDTO,
summary="Get DODA by ID with all details",
)
async def get_doda(
sys_id: int,
async def get_doda_detail(
doda_id: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Get a DODA by its ID with all related data"""
doda = DodaService.get_by_id(db, sys_id)
tenant_id = validate_access_to_resource(db, company_id, current_user)
doda = DodaService.get_by_id(db, doda_id, tenant_id, company_id)
if not doda:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -81,88 +68,34 @@ async def get_doda(
return DodaDetailResponseDTO.model_validate(doda)
@router.post(
"",
response_model=DodaResponseDTO,
status_code=status.HTTP_201_CREATED,
summary="Create DODA",
)
async def create_doda(
doda_data: DodaCreateDTO,
db: Session = Depends(get_core_db),
):
"""Create a new DODA"""
doda = DodaService.create(db, doda_data)
return DodaResponseDTO.model_validate(doda)
@router.put(
"/{sys_id}",
response_model=DodaResponseDTO,
summary="Update DODA",
)
async def update_doda(
sys_id: int,
doda_data: DodaUpdateDTO,
db: Session = Depends(get_core_db),
):
"""Update a DODA"""
doda = DodaService.update(db, sys_id, doda_data)
if not doda:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="DODA not found",
)
return DodaResponseDTO.model_validate(doda)
@router.delete(
"/{sys_id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Delete DODA",
)
async def delete_doda(
sys_id: int,
db: Session = Depends(get_core_db),
):
"""Delete a DODA"""
success = DodaService.delete(db, sys_id)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="DODA not found",
)
return None
# ============ CONTAINERS ENDPOINTS ============
@router.get(
"/{sys_id}/containers",
"/{doda_id}/containers",
response_model=List[DodaContainerResponseDTO],
summary="Get containers for DODA",
)
async def get_doda_containers(
sys_id: int,
doda_id: int,
db: Session = Depends(get_core_db),
):
"""Get all containers for a specific DODA"""
containers = DodaService.get_containers(db, sys_id)
containers = DodaService.get_containers(db, doda_id)
return [DodaContainerResponseDTO.model_validate(c) for c in containers]
@router.post(
"/{sys_id}/containers",
"/{doda_id}/containers",
response_model=DodaContainerResponseDTO,
status_code=status.HTTP_201_CREATED,
summary="Add container to DODA",
)
async def add_container(
sys_id: int,
doda_id: int,
container_data: DodaContainerCreateDTO,
db: Session = Depends(get_core_db),
):
"""Add a new container to a DODA"""
container = DodaService.add_container(db, sys_id, container_data)
container = DodaService.add_container(db, doda_id, container_data)
if not container:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -172,19 +105,19 @@ async def add_container(
@router.put(
"/{sys_id}/containers/{container_line}",
"/{doda_id}/containers/{container_line}",
response_model=DodaContainerResponseDTO,
summary="Update container",
)
async def update_container(
sys_id: int,
doda_id: int,
container_line: int,
container_data: DodaContainerUpdateDTO,
db: Session = Depends(get_core_db),
):
"""Update a container"""
container = DodaService.update_container(
db, sys_id, container_line, container_data
db, doda_id, container_line, container_data
)
if not container:
raise HTTPException(
@@ -196,32 +129,32 @@ async def update_container(
# ============ AMERICAN PEDIMENTOS ENDPOINTS ============
@router.get(
"/{sys_id}/american-pedimentos",
"/{doda_id}/american-pedimentos",
response_model=List[DodaAmericanPedimentoResponseDTO],
summary="Get American pedimentos for DODA",
)
async def get_american_pedimentos(
sys_id: int,
doda_id: int,
db: Session = Depends(get_core_db),
):
"""Get all American pedimentos for a specific DODA"""
pedimentos = DodaService.get_american_pedimentos(db, sys_id)
pedimentos = DodaService.get_american_pedimentos(db, doda_id)
return [DodaAmericanPedimentoResponseDTO.model_validate(p) for p in pedimentos]
@router.post(
"/{sys_id}/american-pedimentos",
"/{doda_id}/american-pedimentos",
response_model=DodaAmericanPedimentoResponseDTO,
status_code=status.HTTP_201_CREATED,
summary="Add American pedimento to DODA",
)
async def add_american_pedimento(
sys_id: int,
doda_id: int,
pedimento_data: DodaAmericanPedimentoCreateDTO,
db: Session = Depends(get_core_db),
):
"""Add a new American pedimento to a DODA"""
pedimento = DodaService.add_american_pedimento(db, sys_id, pedimento_data)
pedimento = DodaService.add_american_pedimento(db, doda_id, pedimento_data)
if not pedimento:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -232,32 +165,32 @@ async def add_american_pedimento(
# ============ PEDIMENTOS ENDPOINTS ============
@router.get(
"/{sys_id}/pedimentos",
"/{doda_id}/pedimentos",
response_model=List[DodaPedimentoResponseDTO],
summary="Get pedimentos for DODA",
)
async def get_doda_pedimentos(
sys_id: int,
doda_id: int,
db: Session = Depends(get_core_db),
):
"""Get all pedimentos for a specific DODA"""
pedimentos = DodaService.get_pedimentos(db, sys_id)
pedimentos = DodaService.get_pedimentos(db, doda_id)
return [DodaPedimentoResponseDTO.model_validate(p) for p in pedimentos]
@router.post(
"/{sys_id}/pedimentos",
"/{doda_id}/pedimentos",
response_model=DodaPedimentoResponseDTO,
status_code=status.HTTP_201_CREATED,
summary="Add pedimento to DODA",
)
async def add_pedimento(
sys_id: int,
doda_id: int,
pedimento_data: DodaPedimentoCreateDTO,
db: Session = Depends(get_core_db),
):
"""Add a new pedimento to a DODA"""
pedimento = DodaService.add_pedimento(db, sys_id, pedimento_data)
pedimento = DodaService.add_pedimento(db, doda_id, pedimento_data)
if not pedimento:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,

View File

@@ -34,19 +34,20 @@ logger = logging.getLogger(__name__)
class DodaService:
"""Servicio para gestión de DODA"""
def __init__(self, db: Session):
self.db = db
# ============ DODA MAIN CRUD ============
@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[Doda], int]:
"""Get all DODAs with pagination"""
query = db.query(Doda)
query = db.query(Doda).filter(
Doda.tenant_id == tenant_id,
Doda.company_id == company_id
)
if filters:
if filters.get("integration_number"):
@@ -66,15 +67,27 @@ class DodaService:
return dodas, total
@staticmethod
def get_by_id(db: Session, sys_id: int) -> Optional[Doda]:
def get_by_id(
db: Session, id: int, tenant_id: int, company_id: int
) -> Optional[Doda]:
"""Get DODA by ID"""
return db.query(Doda).filter(Doda.sys_id == sys_id).first()
return db.query(Doda).filter(
Doda.id == id,
Doda.tenant_id == tenant_id,
Doda.company_id == company_id
).first()
@staticmethod
def create(db: Session, doda_data: DodaCreateDTO) -> Doda:
def create(
db: Session, doda_data: DodaCreateDTO, tenant_id: int, company_id: int
) -> Doda:
"""Create a new DODA"""
try:
db_doda = Doda(**doda_data.model_dump(exclude_unset=True))
db_doda = Doda(
**doda_data.model_dump(exclude_unset=True),
tenant_id=tenant_id,
company_id=company_id
)
db.add(db_doda)
db.commit()
db.refresh(db_doda)
@@ -89,10 +102,12 @@ class DodaService:
raise HTTPException(status_code=500, detail="Error creating DODA")
@staticmethod
def update(db: Session, sys_id: int, doda_data: DodaUpdateDTO) -> Optional[Doda]:
def update(
db: Session, id: int, tenant_id: int, doda_data: DodaUpdateDTO, company_id: int
) -> Optional[Doda]:
"""Update a DODA"""
try:
db_doda = db.query(Doda).filter(Doda.sys_id == sys_id).first()
db_doda = DodaService.get_by_id(db, id, tenant_id, company_id)
if not db_doda:
return None
@@ -112,10 +127,12 @@ class DodaService:
raise HTTPException(status_code=500, detail="Error updating DODA")
@staticmethod
def delete(db: Session, sys_id: int) -> bool:
def delete(
db: Session, id: int, tenant_id: int, company_id: int
) -> bool:
"""Delete a DODA"""
try:
db_doda = db.query(Doda).filter(Doda.sys_id == sys_id).first()
db_doda = DodaService.get_by_id(db, id, tenant_id, company_id)
if not db_doda:
return False
@@ -130,23 +147,23 @@ class DodaService:
# ============ CONTAINERS ============
@staticmethod
def add_container(
db: Session, sys_id: int, container_data: DodaContainerCreateDTO
db: Session, doda_id: int, container_data: DodaContainerCreateDTO
) -> Optional[DodaContainer]:
"""Add a container to a DODA"""
try:
doda = db.query(Doda).filter(Doda.sys_id == sys_id).first()
doda = db.query(Doda).filter(Doda.id == doda_id).first()
if not doda:
return None
# Get max line number
max_line = (
db.query(DodaContainer)
.filter(DodaContainer.doda_sys_id == sys_id)
.filter(DodaContainer.doda_id == doda_id)
.count()
)
db_container = DodaContainer(
doda_sys_id=sys_id,
doda_id=doda_id,
container_line=max_line + 1,
**{
k: v
@@ -167,7 +184,7 @@ class DodaService:
@staticmethod
def update_container(
db: Session,
sys_id: int,
doda_id: int,
container_line: int,
container_data: DodaContainerUpdateDTO,
) -> Optional[DodaContainer]:
@@ -176,7 +193,7 @@ class DodaService:
db_container = (
db.query(DodaContainer)
.filter(
DodaContainer.doda_sys_id == sys_id,
DodaContainer.doda_id == doda_id,
DodaContainer.container_line == container_line,
)
.first()
@@ -197,33 +214,33 @@ class DodaService:
status_code=500, detail="Error updating container")
@staticmethod
def get_containers(db: Session, sys_id: int) -> List[DodaContainer]:
def get_containers(db: Session, doda_id: int) -> List[DodaContainer]:
"""Get all containers for a DODA"""
return (
db.query(DodaContainer)
.filter(DodaContainer.doda_sys_id == sys_id)
.filter(DodaContainer.doda_id == doda_id)
.all()
)
# ============ AMERICAN PEDIMENTOS ============
@staticmethod
def add_american_pedimento(
db: Session, sys_id: int, pedimento_data: DodaAmericanPedimentoCreateDTO
db: Session, doda_id: int, pedimento_data: DodaAmericanPedimentoCreateDTO
) -> Optional[DodaAmericanPedimento]:
"""Add an American pedimento to a DODA"""
try:
doda = db.query(Doda).filter(Doda.sys_id == sys_id).first()
doda = db.query(Doda).filter(Doda.id == doda_id).first()
if not doda:
return None
max_line = (
db.query(DodaAmericanPedimento)
.filter(DodaAmericanPedimento.doda_sys_id == sys_id)
.filter(DodaAmericanPedimento.doda_id == doda_id)
.count()
)
db_pedimento = DodaAmericanPedimento(
doda_sys_id=sys_id,
doda_id=doda_id,
american_pedimento_line=max_line + 1,
**pedimento_data.model_dump(exclude_unset=True),
)
@@ -239,33 +256,33 @@ class DodaService:
)
@staticmethod
def get_american_pedimentos(db: Session, sys_id: int) -> List[DodaAmericanPedimento]:
def get_american_pedimentos(db: Session, doda_id: int) -> List[DodaAmericanPedimento]:
"""Get all American pedimentos for a DODA"""
return (
db.query(DodaAmericanPedimento)
.filter(DodaAmericanPedimento.doda_sys_id == sys_id)
.filter(DodaAmericanPedimento.doda_id == doda_id)
.all()
)
# ============ PEDIMENTOS ============
@staticmethod
def add_pedimento(
db: Session, sys_id: int, pedimento_data: DodaPedimentoCreateDTO
db: Session, doda_id: int, pedimento_data: DodaPedimentoCreateDTO
) -> Optional[DodaPedimento]:
"""Add a pedimento to a DODA"""
try:
doda = db.query(Doda).filter(Doda.sys_id == sys_id).first()
doda = db.query(Doda).filter(Doda.id == doda_id).first()
if not doda:
return None
max_line = (
db.query(DodaPedimento)
.filter(DodaPedimento.doda_sys_id == sys_id)
.filter(DodaPedimento.doda_id == doda_id)
.count()
)
db_pedimento = DodaPedimento(
doda_sys_id=sys_id,
doda_id=doda_id,
pedimento_line=max_line + 1,
**pedimento_data.model_dump(exclude_unset=True),
)
@@ -280,9 +297,9 @@ class DodaService:
status_code=500, detail="Error adding pedimento")
@staticmethod
def get_pedimentos(db: Session, sys_id: int) -> List[DodaPedimento]:
def get_pedimentos(db: Session, doda_id: int) -> List[DodaPedimento]:
"""Get all pedimentos for a DODA"""
return (
db.query(DodaPedimento).filter(
DodaPedimento.doda_sys_id == sys_id).all()
DodaPedimento.doda_id == doda_id).all()
)

View File

@@ -2,12 +2,14 @@
Rutas para gestión de avisos electrónicos
"""
from core.security import get_current_user, validate_access_to_resource
from typing import List
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from core.database import get_core_db
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from .dto import (
ElectronicNoticeCreateDTO,
ElectronicNoticeResponseDTO,
@@ -16,114 +18,17 @@ from .dto import (
from .models import ElectronicNotice
from .service import ElectronicNoticeService
router = APIRouter(prefix="/electronic-notices", tags=["electronic-notices"])
@router.get(
"",
response_model=dict,
summary="Get all electronic notices",
)
async def get_all_notices(
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
notice_number: str = Query(None),
status: str = Query(None),
pedimento: str = Query(None),
db: Session = Depends(get_core_db),
):
"""Get all electronic notices with optional filtering and pagination"""
filters = {}
if notice_number:
filters["notice_number"] = notice_number
if status:
filters["status"] = status
if pedimento:
filters["pedimento"] = pedimento
notices, total = ElectronicNoticeService.get_all(db, skip, limit, filters)
return {
"data": [
ElectronicNoticeResponseDTO.model_validate(notice) for notice in notices
],
"total": total,
"skip": skip,
"limit": limit,
}
@router.get(
"/{sys_id}",
response_model=ElectronicNoticeResponseDTO,
summary="Get electronic notice by ID",
)
async def get_notice(
sys_id: int,
db: Session = Depends(get_core_db),
):
"""Get an electronic notice by its ID"""
notice = ElectronicNoticeService.get_by_id(db, sys_id)
if not notice:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Electronic notice not found",
)
return ElectronicNoticeResponseDTO.model_validate(notice)
@router.post(
"",
response_model=ElectronicNoticeResponseDTO,
status_code=status.HTTP_201_CREATED,
summary="Create electronic notice",
)
async def create_notice(
notice_data: ElectronicNoticeCreateDTO,
db: Session = Depends(get_core_db),
):
"""Create a new electronic notice"""
notice = ElectronicNoticeService.create(db, notice_data)
return ElectronicNoticeResponseDTO.model_validate(notice)
@router.put(
"/{sys_id}",
response_model=ElectronicNoticeResponseDTO,
summary="Update electronic notice",
)
async def update_notice(
sys_id: int,
notice_data: ElectronicNoticeUpdateDTO,
db: Session = Depends(get_core_db),
):
"""Update an electronic notice"""
notice = ElectronicNoticeService.update(db, sys_id, notice_data)
if not notice:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Electronic notice not found",
)
return ElectronicNoticeResponseDTO.model_validate(notice)
@router.delete(
"/{sys_id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Delete electronic notice",
)
async def delete_notice(
sys_id: int,
db: Session = Depends(get_core_db),
):
"""Delete an electronic notice"""
success = ElectronicNoticeService.delete(db, sys_id)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Electronic notice not found",
)
return None
router = TenantCRUDRoutes(
service=ElectronicNoticeService,
create_schema=ElectronicNoticeCreateDTO,
update_schema=ElectronicNoticeUpdateDTO,
response_schema=ElectronicNoticeResponseDTO,
prefix="/electronic-notices",
tags=["electronic-notices"],
resource_name="Electronic Notice",
enable_list=True,
enable_filters=True,
).router
@router.get(
@@ -133,10 +38,14 @@ async def delete_notice(
)
async def get_notices_by_pedimento(
pedimento: str,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Get all electronic notices for a specific pedimento"""
notices = ElectronicNoticeService.get_by_pedimento(db, pedimento)
tenant_id = validate_access_to_resource(db, company_id, current_user)
notices = ElectronicNoticeService.get_by_pedimento(
db, pedimento, tenant_id, company_id)
return [ElectronicNoticeResponseDTO.model_validate(notice) for notice in notices]
@@ -147,8 +56,12 @@ async def get_notices_by_pedimento(
)
async def get_notices_by_status(
status: str,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Get all electronic notices with a specific status"""
notices = ElectronicNoticeService.get_by_status(db, status)
tenant_id = validate_access_to_resource(db, company_id, current_user)
notices = ElectronicNoticeService.get_by_status(
db, status, tenant_id, company_id)
return [ElectronicNoticeResponseDTO.model_validate(notice) for notice in notices]

View File

@@ -22,18 +22,20 @@ logger = logging.getLogger(__name__)
class ElectronicNoticeService:
"""Servicio para gestión de avisos electrónicos"""
def __init__(self, db: Session):
self.db = db
@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[ElectronicNotice], int]:
"""Get all electronic notices with pagination"""
query = db.query(ElectronicNotice)
query = db.query(ElectronicNotice).filter(
ElectronicNotice.tenant_id == tenant_id,
ElectronicNotice.company_id == company_id
)
# Apply filters if provided
if filters:
@@ -59,20 +61,26 @@ class ElectronicNoticeService:
return notices, total
@staticmethod
def get_by_id(db: Session, sys_id: int) -> Optional[ElectronicNotice]:
def get_by_id(
db: Session, id: int, tenant_id: int, company_id: int
) -> Optional[ElectronicNotice]:
"""Get electronic notice by ID"""
return db.query(ElectronicNotice).filter(
ElectronicNotice.sys_id == sys_id
ElectronicNotice.id == id,
ElectronicNotice.tenant_id == tenant_id,
ElectronicNotice.company_id == company_id
).first()
@staticmethod
def create(
db: Session, notice_data: ElectronicNoticeCreateDTO
db: Session, notice_data: ElectronicNoticeCreateDTO, tenant_id: int, company_id: int
) -> ElectronicNotice:
"""Create a new electronic notice"""
try:
db_notice = ElectronicNotice(
**notice_data.model_dump(exclude_unset=True)
**notice_data.model_dump(exclude_unset=True),
tenant_id=tenant_id,
company_id=company_id
)
db.add(db_notice)
@@ -98,16 +106,12 @@ class ElectronicNoticeService:
@staticmethod
def update(
db: Session,
sys_id: int,
notice_data: ElectronicNoticeUpdateDTO,
db: Session, id: int, tenant_id: int, notice_data: ElectronicNoticeUpdateDTO, company_id: int
) -> Optional[ElectronicNotice]:
"""Update an electronic notice"""
try:
db_notice = db.query(ElectronicNotice).filter(
ElectronicNotice.sys_id == sys_id
).first()
db_notice = ElectronicNoticeService.get_by_id(
db, id, tenant_id, company_id)
if not db_notice:
return None
@@ -116,7 +120,6 @@ class ElectronicNoticeService:
db.commit()
db.refresh(db_notice)
return db_notice
except IntegrityError as e:
@@ -135,19 +138,18 @@ class ElectronicNoticeService:
)
@staticmethod
def delete(db: Session, sys_id: int) -> bool:
def delete(
db: Session, id: int, tenant_id: int, company_id: int
) -> bool:
"""Delete an electronic notice"""
try:
db_notice = db.query(ElectronicNotice).filter(
ElectronicNotice.sys_id == sys_id
).first()
db_notice = ElectronicNoticeService.get_by_id(
db, id, tenant_id, company_id)
if not db_notice:
return False
db.delete(db_notice)
db.commit()
return True
except Exception as e:
@@ -159,20 +161,30 @@ class ElectronicNoticeService:
@staticmethod
def get_by_pedimento(
db: Session, pedimento: str
db: Session, pedimento: str, tenant_id: int, company_id: int
) -> List[ElectronicNotice]:
"""Get all electronic notices by pedimento"""
return (
db.query(ElectronicNotice)
.filter(ElectronicNotice.pedimento == pedimento)
.filter(
ElectronicNotice.pedimento == pedimento,
ElectronicNotice.tenant_id == tenant_id,
ElectronicNotice.company_id == company_id
)
.all()
)
@staticmethod
def get_by_status(db: Session, status: str) -> List[ElectronicNotice]:
def get_by_status(
db: Session, status: str, tenant_id: int, company_id: int
) -> List[ElectronicNotice]:
"""Get all electronic notices by status"""
return (
db.query(ElectronicNotice)
.filter(ElectronicNotice.status == status)
.filter(
ElectronicNotice.status == status,
ElectronicNotice.tenant_id == tenant_id,
ElectronicNotice.company_id == company_id
)
.all()
)

View File

@@ -1,14 +1,16 @@
from typing import Optional, List
from sqlalchemy import Integer, String, ForeignKey, UniqueConstraint
from sqlalchemy import Integer, String, ForeignKey, UniqueConstraint, ForeignKeyConstraint
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 Equivalency(Base):
class Equivalency(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "equivalencies"
__table_args__ = (
UniqueConstraint("identifier", name="uq_equivalency_identifier"),
UniqueConstraint("identifier", "tenant_id", "company_id",
name="uq_equivalency_identifier"),
{"schema": "a76"}
)
@@ -22,11 +24,16 @@ class Equivalency(Base):
back_populates="equivalency", cascade="all, delete-orphan")
class EquivalencyItem(Base):
class EquivalencyItem(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "equivalency_items"
__table_args__ = (
UniqueConstraint("equivalency_id", "original_field",
"external_field", name="uq_equivalency_item_fields"),
"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"}
)
@@ -34,8 +41,8 @@ class EquivalencyItem(Base):
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), ForeignKey(
"a76.units_of_measure.code"), nullable=False) # Relation to Unit of Measure
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)
equivalency: Mapped["Equivalency"] = relationship(back_populates="items")

View File

@@ -1,106 +1,99 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from typing import List
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 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
)
from .service import EquivalencyService, EquivalencyItemService
router = APIRouter(prefix="/equivalencies",
tags=["a76.general_catalogs.equivalencies"])
# Equivalency Routes
# 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
item_crud = TenantCRUDRoutes(
service=EquivalencyItemService,
create_schema=EquivalencyItemCreate,
update_schema=EquivalencyItemUpdate,
response_schema=EquivalencyItemResponse,
prefix="/items",
tags=["Equivalency Items"],
resource_name="EquivalencyItem",
enable_list=True,
)
# Custom endpoint for creating items nested under equivalency
@router.post("/", response_model=EquivalencyResponse, status_code=status.HTTP_201_CREATED)
def create_equivalency(
data: EquivalencyCreate,
session: Session = Depends(get_core_db)
):
return service.create_equivalency(session, data)
@router.get("/{id}", response_model=EquivalencyResponse)
def get_equivalency(
id: int,
session: Session = Depends(get_core_db)
):
db_obj = service.get_equivalency(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="Equivalency not found")
return db_obj
@router.get("/", response_model=List[EquivalencyResponse])
def get_equivalencies(
skip: int = 0,
limit: int = 100,
session: Session = Depends(get_core_db)
):
return service.get_equivalencies(session, skip, limit)
@router.put("/{id}", response_model=EquivalencyResponse)
def update_equivalency(
id: int,
data: EquivalencyUpdate,
session: Session = Depends(get_core_db)
):
db_obj = service.get_equivalency(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="Equivalency not found")
return service.update_equivalency(session, db_obj, data)
@router.delete("/{id}", response_model=EquivalencyResponse)
def delete_equivalency(
id: int,
session: Session = Depends(get_core_db)
):
db_obj = service.get_equivalency(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="Equivalency not found")
return service.delete_equivalency(session, db_obj)
# Equivalency Item Routes
@router.post("/{equivalency_id}/items", response_model=EquivalencyItemResponse, status_code=status.HTTP_201_CREATED)
def create_equivalency_item(
@equivalency_crud.router.post(
"/{equivalency_id}/items",
response_model=EquivalencyItemResponse,
status_code=status.HTTP_201_CREATED,
summary="Create equivalency item",
)
async def create_equivalency_item(
equivalency_id: int,
data: EquivalencyItemCreate,
session: Session = Depends(get_core_db)
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 = service.get_equivalency(session, equivalency_id)
parent = EquivalencyService.get_by_id(
db, equivalency_id, tenant_id, company_id)
if not parent:
raise HTTPException(status_code=404, detail="Equivalency not found")
return service.create_equivalency_item(session, equivalency_id, data)
# 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.put("/items/{id}", response_model=EquivalencyItemResponse)
def update_equivalency_item(
id: int,
data: EquivalencyItemUpdate,
session: Session = Depends(get_core_db)
):
db_obj = service.get_equivalency_item(session, id)
if not db_obj:
raise HTTPException(
status_code=404, detail="Equivalency Item not found")
return service.update_equivalency_item(session, db_obj, data)
@router.delete("/items/{id}", response_model=EquivalencyItemResponse)
def delete_equivalency_item(
id: int,
session: Session = Depends(get_core_db)
):
db_obj = service.get_equivalency_item(session, id)
if not db_obj:
raise HTTPException(
status_code=404, detail="Equivalency Item not found")
return service.delete_equivalency_item(session, db_obj)
router.include_router(equivalency_crud.router)
router.include_router(item_crud.router)

View File

@@ -1,83 +1,228 @@
from typing import List, Optional, Tuple, Dict, Any
from sqlalchemy.orm import Session
from sqlalchemy import select
from typing import Sequence, Optional
from fastapi import HTTPException
from .models import Equivalency, EquivalencyItem
from .dto import EquivalencyCreate, EquivalencyUpdate, EquivalencyItemCreate, EquivalencyItemUpdate
# Equivalency Services
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:
# Add filters here if needed
pass
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:
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)
if data.items:
for item_data in data.items:
item = EquivalencyItem(
**item_data.model_dump(),
equivalency_id=db_obj.id,
tenant_id=tenant_id,
company_id=company_id
)
db.add(item)
db.commit()
db.refresh(db_obj)
return db_obj
@staticmethod
def update(
db: Session,
id: int,
data: EquivalencyUpdate,
tenant_id: int,
company_id: int
) -> Optional[Equivalency]:
db_obj = EquivalencyService.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 != 'items': # Handle items separately if needed, or ignore for now as per original code
setattr(db_obj, key, value)
db.commit()
db.refresh(db_obj)
return db_obj
@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
def create_equivalency(session: Session, data: EquivalencyCreate) -> Equivalency:
db_obj = Equivalency(identifier=data.identifier,
description=data.description)
session.add(db_obj)
session.commit()
session.refresh(db_obj)
class EquivalencyItemService:
@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[EquivalencyItem], int]:
query = db.query(EquivalencyItem).filter(
EquivalencyItem.tenant_id == tenant_id,
EquivalencyItem.company_id == company_id
)
if data.items:
for item_data in data.items:
item = EquivalencyItem(
**item_data.model_dump(), equivalency_id=db_obj.id)
session.add(item)
session.commit()
session.refresh(db_obj)
if filters and "equivalency_id" in filters:
query = query.filter(
EquivalencyItem.equivalency_id == filters["equivalency_id"])
return db_obj
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[EquivalencyItem]:
return db.query(EquivalencyItem).filter(
EquivalencyItem.id == id,
EquivalencyItem.tenant_id == tenant_id,
EquivalencyItem.company_id == company_id
).first()
def get_equivalency(session: Session, id: int) -> Optional[Equivalency]:
stmt = select(Equivalency).where(Equivalency.id == id)
result = session.execute(stmt)
return result.scalars().first()
@staticmethod
def create(
db: Session,
data: EquivalencyItemCreate,
tenant_id: int,
company_id: int
) -> EquivalencyItem:
# Note: equivalency_id should be in data or handled by the caller if it's a nested route
# But TenantCRUDRoutes for child resources might pass it in filters or we need to handle it.
# For now, assuming it's in data or we don't use child resource feature yet.
# If using child resource, the parent_id is usually passed in the path.
# But TenantCRUDRoutes passes the body.
def get_equivalencies(session: Session, skip: int = 0, limit: int = 100) -> Sequence[Equivalency]:
stmt = select(Equivalency).offset(skip).limit(limit)
result = session.execute(stmt)
return result.scalars().all()
db_obj = EquivalencyItem(
**data.model_dump(),
tenant_id=tenant_id,
company_id=company_id
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
@staticmethod
def create_nested(
db: Session,
equivalency_id: int,
data: EquivalencyItemCreate,
tenant_id: int,
company_id: int
) -> EquivalencyItem:
db_obj = EquivalencyItem(
**data.model_dump(),
equivalency_id=equivalency_id,
tenant_id=tenant_id,
company_id=company_id
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update_equivalency(session: Session, db_obj: Equivalency, update_data: EquivalencyUpdate) -> Equivalency:
update_dict = update_data.model_dump(exclude_unset=True)
for key, value in update_dict.items():
setattr(db_obj, key, value)
session.commit()
session.refresh(db_obj)
return db_obj
@staticmethod
def update(
db: Session,
id: int,
data: EquivalencyItemUpdate,
tenant_id: int,
company_id: int
) -> Optional[EquivalencyItem]:
db_obj = EquivalencyItemService.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():
setattr(db_obj, key, value)
def delete_equivalency(session: Session, db_obj: Equivalency) -> Equivalency:
session.delete(db_obj)
session.commit()
return db_obj
db.commit()
db.refresh(db_obj)
return db_obj
# Equivalency Item Services
@staticmethod
def delete(
db: Session,
id: int,
tenant_id: int,
company_id: int
) -> bool:
db_obj = EquivalencyItemService.get_by_id(
db, id, tenant_id, company_id)
if not db_obj:
return False
def create_equivalency_item(session: Session, equivalency_id: int, data: EquivalencyItemCreate) -> EquivalencyItem:
db_obj = EquivalencyItem(
**data.model_dump(), equivalency_id=equivalency_id)
session.add(db_obj)
session.commit()
session.refresh(db_obj)
return db_obj
def get_equivalency_item(session: Session, id: int) -> Optional[EquivalencyItem]:
return session.get(EquivalencyItem, id)
def update_equivalency_item(session: Session, db_obj: EquivalencyItem, update_data: EquivalencyItemUpdate) -> EquivalencyItem:
update_dict = update_data.model_dump(exclude_unset=True)
for key, value in update_dict.items():
setattr(db_obj, key, value)
session.commit()
session.refresh(db_obj)
return db_obj
def delete_equivalency_item(session: Session, db_obj: EquivalencyItem) -> EquivalencyItem:
session.delete(db_obj)
session.commit()
return db_obj
db.delete(db_obj)
db.commit()
return True

View File

@@ -4,10 +4,12 @@ Rutas para gestión de catálogos de errores
from typing import List
from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from core.database import get_core_db
from core.security import get_current_user
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from .dto import (
ErrorClassificationCreateDTO,
ErrorClassificationResponseDTO,
@@ -23,73 +25,39 @@ from .service import ErrorClassificationService, ErrorCatalogService
router = APIRouter(prefix="/error-catalogs", tags=["error-catalogs"])
# ============ ERROR CLASSIFICATIONS ENDPOINTS ============
@router.get(
"/classifications",
response_model=dict,
summary="Get all error classifications",
classification_crud = TenantCRUDRoutes(
service=ErrorClassificationService,
create_schema=ErrorClassificationCreateDTO,
update_schema=ErrorClassificationUpdateDTO,
response_schema=ErrorClassificationResponseDTO,
prefix="/classifications",
tags=["error-classifications"],
resource_name="ErrorClassification",
enable_list=True,
)
async def get_all_classifications(
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
code: str = Query(None),
level: str = Query(None),
db: Session = Depends(get_core_db),
):
"""Get all error classifications with optional filtering and pagination"""
filters = {}
if code:
filters["code"] = code
if level:
filters["level"] = level
classifications, total = ErrorClassificationService.get_all(
db, skip, limit, filters
)
return {
"data": [
ErrorClassificationResponseDTO.model_validate(classification)
for classification in classifications
],
"total": total,
"skip": skip,
"limit": limit,
}
# Add custom endpoints for classifications
@router.get(
"/classifications/{classification_id}",
response_model=ErrorClassificationDetailResponseDTO,
summary="Get error classification by ID with errors",
)
async def get_classification(
classification_id: int,
db: Session = Depends(get_core_db),
):
"""Get an error classification by its ID with all related errors"""
classification = ErrorClassificationService.get_by_id(
db, classification_id)
if not classification:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Classification not found",
)
return ErrorClassificationDetailResponseDTO.model_validate(classification)
@router.get(
"/classifications/code/{code}",
@classification_crud.router.get(
"/code/{code}",
response_model=ErrorClassificationDetailResponseDTO,
summary="Get error classification by code with errors",
)
async def get_classification_by_code(
code: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Get an error classification by its code with all related errors"""
classification = ErrorClassificationService.get_by_code(db, code)
classification = ErrorClassificationService.get_by_code(
db,
code,
tenant_id=current_user["tenant_id"],
company_id=current_user["company_id"]
)
if not classification:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -97,118 +65,53 @@ async def get_classification_by_code(
)
return ErrorClassificationDetailResponseDTO.model_validate(classification)
# Override get_by_id to return detail DTO
@router.post(
"/classifications",
response_model=ErrorClassificationResponseDTO,
status_code=status.HTTP_201_CREATED,
summary="Create error classification",
@classification_crud.router.get(
"/{id}",
response_model=ErrorClassificationDetailResponseDTO,
summary="Get error classification by ID with errors",
)
async def create_classification(
classification_data: ErrorClassificationCreateDTO,
async def get_classification(
id: int,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Create a new error classification"""
classification = ErrorClassificationService.create(db, classification_data)
return ErrorClassificationResponseDTO.model_validate(classification)
@router.put(
"/classifications/{classification_id}",
response_model=ErrorClassificationResponseDTO,
summary="Update error classification",
)
async def update_classification(
classification_id: int,
classification_data: ErrorClassificationUpdateDTO,
db: Session = Depends(get_core_db),
):
"""Update an error classification"""
classification = ErrorClassificationService.update(
db, classification_id, classification_data
"""Get an error classification by its ID with all related errors"""
classification = ErrorClassificationService.get_by_id(
db,
id,
tenant_id=current_user["tenant_id"],
company_id=current_user["company_id"]
)
if not classification:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Classification not found",
)
return ErrorClassificationResponseDTO.model_validate(classification)
return ErrorClassificationDetailResponseDTO.model_validate(classification)
@router.delete(
"/classifications/{classification_id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Delete error classification",
)
async def delete_classification(
classification_id: int,
db: Session = Depends(get_core_db),
):
"""Delete an error classification"""
success = ErrorClassificationService.delete(db, classification_id)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Classification not found",
)
return None
router.include_router(classification_crud.router)
# ============ ERROR CATALOG ENDPOINTS ============
@router.get(
"",
response_model=dict,
summary="Get all errors in catalog",
catalog_crud = TenantCRUDRoutes(
service=ErrorCatalogService,
create_schema=ErrorCatalogCreateDTO,
update_schema=ErrorCatalogUpdateDTO,
response_schema=ErrorCatalogResponseDTO,
prefix="",
tags=["error-catalogs"],
resource_name="ErrorCatalog",
enable_list=True,
)
async def get_all_errors(
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
code: str = Query(None),
description: str = Query(None),
classification_code: str = Query(None),
db: Session = Depends(get_core_db),
):
"""Get all error catalogs with optional filtering and pagination"""
filters = {}
if code:
filters["code"] = code
if description:
filters["description"] = description
if classification_code:
filters["classification_code"] = classification_code
catalogs, total = ErrorCatalogService.get_all(db, skip, limit, filters)
return {
"data": [
ErrorCatalogResponseDTO.model_validate(catalog) for catalog in catalogs
],
"total": total,
"skip": skip,
"limit": limit,
}
# Add custom endpoints for catalogs
@router.get(
"/{error_id}",
response_model=ErrorCatalogDetailResponseDTO,
summary="Get error by ID",
)
async def get_error(
error_id: int,
db: Session = Depends(get_core_db),
):
"""Get an error by its ID with classification details"""
error = ErrorCatalogService.get_by_id(db, error_id)
if not error:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Error not found",
)
return ErrorCatalogDetailResponseDTO.model_validate(error)
@router.get(
@catalog_crud.router.get(
"/code/{code}",
response_model=ErrorCatalogDetailResponseDTO,
summary="Get error by code",
@@ -216,9 +119,15 @@ async def get_error(
async def get_error_by_code(
code: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Get an error by its code with classification details"""
error = ErrorCatalogService.get_by_code(db, code)
error = ErrorCatalogService.get_by_code(
db,
code,
tenant_id=current_user["tenant_id"],
company_id=current_user["company_id"]
)
if not error:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -227,61 +136,7 @@ async def get_error_by_code(
return ErrorCatalogDetailResponseDTO.model_validate(error)
@router.post(
"",
response_model=ErrorCatalogResponseDTO,
status_code=status.HTTP_201_CREATED,
summary="Create error in catalog",
)
async def create_error(
error_data: ErrorCatalogCreateDTO,
db: Session = Depends(get_core_db),
):
"""Create a new error in the catalog"""
error = ErrorCatalogService.create(db, error_data)
return ErrorCatalogResponseDTO.model_validate(error)
@router.put(
"/{error_id}",
response_model=ErrorCatalogResponseDTO,
summary="Update error in catalog",
)
async def update_error(
error_id: int,
error_data: ErrorCatalogUpdateDTO,
db: Session = Depends(get_core_db),
):
"""Update an error in the catalog"""
error = ErrorCatalogService.update(db, error_id, error_data)
if not error:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Error not found",
)
return ErrorCatalogResponseDTO.model_validate(error)
@router.delete(
"/{error_id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Delete error from catalog",
)
async def delete_error(
error_id: int,
db: Session = Depends(get_core_db),
):
"""Delete an error from the catalog"""
success = ErrorCatalogService.delete(db, error_id)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Error not found",
)
return None
@router.get(
@catalog_crud.router.get(
"/classification/{classification_id}",
response_model=List[ErrorCatalogResponseDTO],
summary="Get errors by classification",
@@ -289,7 +144,42 @@ async def delete_error(
async def get_errors_by_classification(
classification_id: int,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Get all errors for a specific classification"""
errors = ErrorCatalogService.get_by_classification(db, classification_id)
errors = ErrorCatalogService.get_by_classification(
db,
classification_id,
tenant_id=current_user["tenant_id"],
company_id=current_user["company_id"]
)
return [ErrorCatalogResponseDTO.model_validate(error) for error in errors]
# Override get_by_id to return detail DTO
@catalog_crud.router.get(
"/{id}",
response_model=ErrorCatalogDetailResponseDTO,
summary="Get error by ID",
)
async def get_error(
id: int,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Get an error by its ID with classification details"""
error = ErrorCatalogService.get_by_id(
db,
id,
tenant_id=current_user["tenant_id"],
company_id=current_user["company_id"]
)
if not error:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Error not found",
)
return ErrorCatalogDetailResponseDTO.model_validate(error)
router.include_router(catalog_crud.router)

View File

@@ -25,18 +25,20 @@ logger = logging.getLogger(__name__)
class ErrorClassificationService:
"""Servicio para gestión de clasificaciones de errores"""
def __init__(self, db: Session):
self.db = db
@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[ErrorClassification], int]:
"""Get all error classifications with pagination"""
query = db.query(ErrorClassification)
query = db.query(ErrorClassification).filter(
ErrorClassification.tenant_id == tenant_id,
ErrorClassification.company_id == company_id
)
if filters:
if filters.get("code"):
@@ -54,31 +56,45 @@ class ErrorClassificationService:
return classifications, total
@staticmethod
def get_by_code(db: Session, code: str) -> Optional[ErrorClassification]:
def get_by_code(
db: Session, code: str, tenant_id: int, company_id: int
) -> Optional[ErrorClassification]:
"""Get error classification by code"""
return (
db.query(ErrorClassification)
.filter(ErrorClassification.code == code)
.filter(
ErrorClassification.code == code,
ErrorClassification.tenant_id == tenant_id,
ErrorClassification.company_id == company_id
)
.first()
)
@staticmethod
def get_by_id(db: Session, classification_id: int) -> Optional[ErrorClassification]:
def get_by_id(
db: Session, id: int, tenant_id: int, company_id: int
) -> Optional[ErrorClassification]:
"""Get error classification by ID"""
return (
db.query(ErrorClassification)
.filter(ErrorClassification.id == classification_id)
.filter(
ErrorClassification.id == id,
ErrorClassification.tenant_id == tenant_id,
ErrorClassification.company_id == company_id
)
.first()
)
@staticmethod
def create(
db: Session, classification_data: ErrorClassificationCreateDTO
db: Session, classification_data: ErrorClassificationCreateDTO, tenant_id: int, company_id: int
) -> ErrorClassification:
"""Create a new error classification"""
try:
db_classification = ErrorClassification(
**classification_data.model_dump(exclude_unset=True)
**classification_data.model_dump(exclude_unset=True),
tenant_id=tenant_id,
company_id=company_id
)
db.add(db_classification)
@@ -104,18 +120,12 @@ class ErrorClassificationService:
@staticmethod
def update(
db: Session,
classification_id: int,
classification_data: ErrorClassificationUpdateDTO,
db: Session, id: int, tenant_id: int, classification_data: ErrorClassificationUpdateDTO, company_id: int
) -> Optional[ErrorClassification]:
"""Update an error classification"""
try:
db_classification = (
db.query(ErrorClassification)
.filter(ErrorClassification.id == classification_id)
.first()
)
db_classification = ErrorClassificationService.get_by_id(
db, id, tenant_id, company_id)
if not db_classification:
return None
@@ -124,7 +134,6 @@ class ErrorClassificationService:
db.commit()
db.refresh(db_classification)
return db_classification
except IntegrityError as e:
@@ -143,21 +152,18 @@ class ErrorClassificationService:
)
@staticmethod
def delete(db: Session, classification_id: int) -> bool:
def delete(
db: Session, id: int, tenant_id: int, company_id: int
) -> bool:
"""Delete an error classification"""
try:
db_classification = (
db.query(ErrorClassification)
.filter(ErrorClassification.id == classification_id)
.first()
)
db_classification = ErrorClassificationService.get_by_id(
db, id, tenant_id, company_id)
if not db_classification:
return False
db.delete(db_classification)
db.commit()
return True
except Exception as e:
@@ -171,18 +177,20 @@ class ErrorClassificationService:
class ErrorCatalogService:
"""Servicio para gestión de catálogos de errores"""
def __init__(self, db: Session):
self.db = db
@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[ErrorCatalog], int]:
"""Get all error catalogs with pagination"""
query = db.query(ErrorCatalog)
query = db.query(ErrorCatalog).filter(
ErrorCatalog.tenant_id == tenant_id,
ErrorCatalog.company_id == company_id
)
if filters:
if filters.get("code"):
@@ -204,35 +212,57 @@ class ErrorCatalogService:
return catalogs, total
@staticmethod
def get_by_code(db: Session, code: str) -> Optional[ErrorCatalog]:
def get_by_code(
db: Session, code: str, tenant_id: int, company_id: int
) -> Optional[ErrorCatalog]:
"""Get error catalog by code"""
return db.query(ErrorCatalog).filter(ErrorCatalog.code == code).first()
return db.query(ErrorCatalog).filter(
ErrorCatalog.code == code,
ErrorCatalog.tenant_id == tenant_id,
ErrorCatalog.company_id == company_id
).first()
@staticmethod
def get_by_id(db: Session, error_id: int) -> Optional[ErrorCatalog]:
def get_by_id(
db: Session, id: int, tenant_id: int, company_id: int
) -> Optional[ErrorCatalog]:
"""Get error catalog by ID"""
return db.query(ErrorCatalog).filter(ErrorCatalog.id == error_id).first()
return db.query(ErrorCatalog).filter(
ErrorCatalog.id == id,
ErrorCatalog.tenant_id == tenant_id,
ErrorCatalog.company_id == company_id
).first()
@staticmethod
def get_by_classification(
db: Session, classification_id: int
db: Session, classification_id: int, tenant_id: int, company_id: int
) -> List[ErrorCatalog]:
"""Get all errors by classification"""
return (
db.query(ErrorCatalog)
.filter(ErrorCatalog.classification_id == classification_id)
.filter(
ErrorCatalog.classification_id == classification_id,
ErrorCatalog.tenant_id == tenant_id,
ErrorCatalog.company_id == company_id
)
.all()
)
@staticmethod
def create(db: Session, error_data: ErrorCatalogCreateDTO) -> ErrorCatalog:
def create(
db: Session, error_data: ErrorCatalogCreateDTO, tenant_id: int, company_id: int
) -> ErrorCatalog:
"""Create a new error catalog"""
try:
# Validate classification exists if provided
if error_data.classification_id:
classification = (
db.query(ErrorClassification)
.filter(ErrorClassification.id == error_data.classification_id)
.filter(
ErrorClassification.id == error_data.classification_id,
ErrorClassification.tenant_id == tenant_id,
ErrorClassification.company_id == company_id
)
.first()
)
if not classification:
@@ -242,7 +272,10 @@ class ErrorCatalogService:
)
db_error = ErrorCatalog(
**error_data.model_dump(exclude_unset=True))
**error_data.model_dump(exclude_unset=True),
tenant_id=tenant_id,
company_id=company_id
)
db.add(db_error)
db.commit()
@@ -263,30 +296,17 @@ class ErrorCatalogService:
db.rollback()
logger.error(f"Error creating error catalog: {str(e)}")
raise HTTPException(
status_code=500, detail="Error creating error catalog")
status_code=500, detail="Error creating error catalog"
)
@staticmethod
def update(
db: Session, error_id: int, error_data: ErrorCatalogUpdateDTO
db: Session, id: int, tenant_id: int, error_data: ErrorCatalogUpdateDTO, company_id: int
) -> Optional[ErrorCatalog]:
"""Update an error catalog"""
try:
# Validate classification exists if provided
if error_data.classification_id:
classification = (
db.query(ErrorClassification)
.filter(ErrorClassification.id == error_data.classification_id)
.first()
)
if not classification:
raise HTTPException(
status_code=400,
detail="Classification not found",
)
db_error = db.query(ErrorCatalog).filter(
ErrorCatalog.id == error_id).first()
db_error = ErrorCatalogService.get_by_id(
db, id, tenant_id, company_id)
if not db_error:
return None
@@ -295,11 +315,8 @@ class ErrorCatalogService:
db.commit()
db.refresh(db_error)
return db_error
except HTTPException:
raise
except IntegrityError as e:
db.rollback()
logger.error(f"IntegrityError updating error catalog: {str(e)}")
@@ -315,22 +332,23 @@ class ErrorCatalogService:
)
@staticmethod
def delete(db: Session, error_id: int) -> bool:
def delete(
db: Session, id: int, tenant_id: int, company_id: int
) -> bool:
"""Delete an error catalog"""
try:
db_error = db.query(ErrorCatalog).filter(
ErrorCatalog.id == error_id).first()
db_error = ErrorCatalogService.get_by_id(
db, id, tenant_id, company_id)
if not db_error:
return False
db.delete(db_error)
db.commit()
return True
except Exception as e:
db.rollback()
logger.error(f"Error deleting error catalog: {str(e)}")
raise HTTPException(
status_code=500, detail="Error deleting error catalog")
status_code=500, detail="Error deleting error catalog"
)

View File

@@ -26,7 +26,8 @@ class ExchangeRateService:
# Apply filters if provided
if filters:
if filters.get("date"):
query = query.filter(models.ExchangeRate.date == filters["date"])
query = query.filter(
models.ExchangeRate.date == filters["date"])
if filters.get("local_currency"):
query = query.filter(
models.ExchangeRate.local_currency == filters["local_currency"]
@@ -37,7 +38,8 @@ class ExchangeRateService:
)
total = query.count()
exchange_rates = query.order_by(models.ExchangeRate.date.desc()).offset(skip).limit(limit).all()
exchange_rates = query.order_by(
models.ExchangeRate.date.desc()).offset(skip).limit(limit).all()
return exchange_rates, total
@@ -76,9 +78,9 @@ class ExchangeRateService:
def update(
db: Session,
exchange_rate_id: int,
exchange_rate_data: dto.ExchangeRateUpdateDTO,
tenant_id: int,
company_id: int,
exchange_rate_data: dto.ExchangeRateUpdateDTO,
) -> Optional[models.ExchangeRate]:
"""Update an exchange rate"""
exchange_rate = ExchangeRateService.get_by_id(

View File

@@ -1,142 +1,36 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from core.database import get_core_db
from core.security import get_current_user, validate_access_to_resource
from . import service
from fastapi import APIRouter
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from .dto import (
IdentifierCreate, IdentifierResponse, IdentifierUpdate,
IdentifierDetailCreate, IdentifierDetailResponse, IdentifierDetailUpdate
)
from .service import IdentifierService, IdentifierDetailService
router = APIRouter(prefix="/identifiers",
tags=["a76.general_catalogs.identifiers"])
# Identifier Routes
# Identifier CRUD
identifier_crud = TenantCRUDRoutes(
create_schema=IdentifierCreate,
update_schema=IdentifierUpdate,
response_schema=IdentifierResponse,
service=IdentifierService,
prefix="",
tags=["Identifiers"],
resource_name="Identifier",
enable_list=True
)
# Identifier Detail CRUD
detail_crud = TenantCRUDRoutes(
create_schema=IdentifierDetailCreate,
update_schema=IdentifierDetailUpdate,
response_schema=IdentifierDetailResponse,
service=IdentifierDetailService,
prefix="/details",
tags=["Identifier Details"],
resource_name="Identifier Detail"
)
@router.post("/", response_model=IdentifierResponse, status_code=status.HTTP_201_CREATED)
def create_identifier(
data: IdentifierCreate,
session: Session = Depends(get_core_db),
current_user=Depends(get_current_user)
):
tenant_id = validate_access_to_resource(
session, data.company_id, current_user)
return service.create_identifier(session, data, tenant_id)
@router.get("/{id}", response_model=IdentifierResponse)
def get_identifier(
id: int,
session: Session = Depends(get_core_db),
current_user=Depends(get_current_user)
):
db_obj = service.get_identifier(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="Identifier not found")
# Validate access to the resource's company
validate_access_to_resource(session, db_obj.company_id, current_user)
return db_obj
@router.get("/", response_model=List[IdentifierResponse])
def get_identifiers(
skip: int = 0,
limit: int = 100,
session: Session = Depends(get_core_db),
current_user=Depends(get_current_user)
):
# Note: Listing usually requires filtering by company/tenant, but for simplicity here we just list.
# In a real scenario, we should filter by tenant_id from token or company_id query param.
# For now, we just return all, assuming the service might filter later or this is admin only.
# But since we need to validate access, we should probably ask for company_id in query.
# However, to keep it simple and consistent with previous modules (which didn't have this check),
# I will just return the list. But the user asked for validation.
# If I don't have a company_id to validate against, I can't validate.
# I'll leave it as is for list, but individual access is validated.
return service.get_identifiers(session, skip, limit)
@router.put("/{id}", response_model=IdentifierResponse)
def update_identifier(
id: int,
data: IdentifierUpdate,
session: Session = Depends(get_core_db),
current_user=Depends(get_current_user)
):
db_obj = service.get_identifier(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="Identifier not found")
validate_access_to_resource(session, db_obj.company_id, current_user)
return service.update_identifier(session, db_obj, data)
@router.delete("/{id}", response_model=IdentifierResponse)
def delete_identifier(
id: int,
session: Session = Depends(get_core_db),
current_user=Depends(get_current_user)
):
db_obj = service.get_identifier(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="Identifier not found")
validate_access_to_resource(session, db_obj.company_id, current_user)
return service.delete_identifier(session, db_obj)
# Identifier Detail Routes
@router.post("/details", response_model=IdentifierDetailResponse, status_code=status.HTTP_201_CREATED)
def create_identifier_detail(
data: IdentifierDetailCreate,
session: Session = Depends(get_core_db),
current_user=Depends(get_current_user)
):
tenant_id = validate_access_to_resource(
session, data.company_id, current_user)
return service.create_identifier_detail(session, data, tenant_id)
@router.get("/details/{id}", response_model=IdentifierDetailResponse)
def get_identifier_detail(
id: int,
session: Session = Depends(get_core_db),
current_user=Depends(get_current_user)
):
db_obj = service.get_identifier_detail(session, id)
if not db_obj:
raise HTTPException(
status_code=404, detail="Identifier Detail not found")
validate_access_to_resource(session, db_obj.company_id, current_user)
return db_obj
@router.put("/details/{id}", response_model=IdentifierDetailResponse)
def update_identifier_detail(
id: int,
data: IdentifierDetailUpdate,
session: Session = Depends(get_core_db),
current_user=Depends(get_current_user)
):
db_obj = service.get_identifier_detail(session, id)
if not db_obj:
raise HTTPException(
status_code=404, detail="Identifier Detail not found")
validate_access_to_resource(session, db_obj.company_id, current_user)
return service.update_identifier_detail(session, db_obj, data)
@router.delete("/details/{id}", response_model=IdentifierDetailResponse)
def delete_identifier_detail(
id: int,
session: Session = Depends(get_core_db),
current_user=Depends(get_current_user)
):
db_obj = service.get_identifier_detail(session, id)
if not db_obj:
raise HTTPException(
status_code=404, detail="Identifier Detail not found")
validate_access_to_resource(session, db_obj.company_id, current_user)
return service.delete_identifier_detail(session, db_obj)
router.include_router(identifier_crud.router)
router.include_router(detail_crud.router)

View File

@@ -1,71 +1,196 @@
from typing import List, Optional, Tuple, Dict, Any
from sqlalchemy.orm import Session
from typing import Sequence, Optional
from sqlalchemy import select
from .models import Identifier, IdentifierDetail
from .dto import IdentifierCreate, IdentifierUpdate, IdentifierDetailCreate, IdentifierDetailUpdate
# Identifier Services
class IdentifierService:
@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[Identifier], int]:
query = db.query(Identifier).filter(
Identifier.tenant_id == tenant_id,
Identifier.company_id == company_id
)
if filters:
# Add filters here if needed
pass
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[Identifier]:
return db.query(Identifier).filter(
Identifier.id == id,
Identifier.tenant_id == tenant_id,
Identifier.company_id == company_id
).first()
@staticmethod
def create(
db: Session,
data: IdentifierCreate,
tenant_id: int,
company_id: int
) -> Identifier:
# Exclude company_id from data as it is passed separately
data_dict = data.model_dump()
if 'company_id' in data_dict:
del data_dict['company_id']
db_obj = Identifier(
**data_dict,
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,
id: int,
data: IdentifierUpdate,
tenant_id: int,
company_id: int
) -> Optional[Identifier]:
db_obj = IdentifierService.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():
setattr(db_obj, key, value)
db.commit()
db.refresh(db_obj)
return db_obj
@staticmethod
def delete(
db: Session,
id: int,
tenant_id: int,
company_id: int
) -> bool:
db_obj = IdentifierService.get_by_id(db, id, tenant_id, company_id)
if not db_obj:
return False
db.delete(db_obj)
db.commit()
return True
def create_identifier(session: Session, data: IdentifierCreate, tenant_id: int) -> Identifier:
db_obj = Identifier(**data.model_dump(), tenant_id=tenant_id)
session.add(db_obj)
session.commit()
session.refresh(db_obj)
return db_obj
class IdentifierDetailService:
@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[IdentifierDetail], int]:
query = db.query(IdentifierDetail).filter(
IdentifierDetail.tenant_id == tenant_id,
IdentifierDetail.company_id == company_id
)
if filters:
# Add filters here if needed
pass
def get_identifier(session: Session, id: int) -> Optional[Identifier]:
return session.get(Identifier, id)
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[IdentifierDetail]:
return db.query(IdentifierDetail).filter(
IdentifierDetail.id == id,
IdentifierDetail.tenant_id == tenant_id,
IdentifierDetail.company_id == company_id
).first()
def get_identifiers(session: Session, skip: int = 0, limit: int = 100) -> Sequence[Identifier]:
return session.query(Identifier).offset(skip).limit(limit).all()
@staticmethod
def create(
db: Session,
data: IdentifierDetailCreate,
tenant_id: int,
company_id: int
) -> IdentifierDetail:
# Exclude company_id from data as it is passed separately
data_dict = data.model_dump()
if 'company_id' in data_dict:
del data_dict['company_id']
db_obj = IdentifierDetail(
**data_dict,
tenant_id=tenant_id,
company_id=company_id
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update_identifier(session: Session, db_obj: Identifier, update_data: IdentifierUpdate) -> Identifier:
update_dict = update_data.model_dump(exclude_unset=True)
for key, value in update_dict.items():
setattr(db_obj, key, value)
session.commit()
session.refresh(db_obj)
return db_obj
@staticmethod
def update(
db: Session,
id: int,
data: IdentifierDetailUpdate,
tenant_id: int,
company_id: int
) -> Optional[IdentifierDetail]:
db_obj = IdentifierDetailService.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():
setattr(db_obj, key, value)
def delete_identifier(session: Session, db_obj: Identifier) -> Identifier:
session.delete(db_obj)
session.commit()
return db_obj
db.commit()
db.refresh(db_obj)
return db_obj
# Identifier Detail Services
@staticmethod
def delete(
db: Session,
id: int,
tenant_id: int,
company_id: int
) -> bool:
db_obj = IdentifierDetailService.get_by_id(
db, id, tenant_id, company_id)
if not db_obj:
return False
def create_identifier_detail(session: Session, data: IdentifierDetailCreate, tenant_id: int) -> IdentifierDetail:
db_obj = IdentifierDetail(**data.model_dump(), tenant_id=tenant_id)
session.add(db_obj)
session.commit()
session.refresh(db_obj)
return db_obj
def get_identifier_detail(session: Session, id: int) -> Optional[IdentifierDetail]:
return session.get(IdentifierDetail, id)
def get_identifier_details(session: Session, skip: int = 0, limit: int = 100) -> Sequence[IdentifierDetail]:
return session.query(IdentifierDetail).offset(skip).limit(limit).all()
def update_identifier_detail(session: Session, db_obj: IdentifierDetail, update_data: IdentifierDetailUpdate) -> IdentifierDetail:
update_dict = update_data.model_dump(exclude_unset=True)
for key, value in update_dict.items():
setattr(db_obj, key, value)
session.commit()
session.refresh(db_obj)
return db_obj
def delete_identifier_detail(session: Session, db_obj: IdentifierDetail) -> IdentifierDetail:
session.delete(db_obj)
session.commit()
return db_obj
db.delete(db_obj)
db.commit()
return True

View File

@@ -9,7 +9,8 @@ from core.database import Base
class INPC(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "inpc"
__table_args__ = (
UniqueConstraint("year", "month", name="uq_inpc_year_month"),
UniqueConstraint("year", "month", "tenant_id",
"company_id", name="uq_inpc_year_month"),
{"schema": "a76"}
)

View File

@@ -1,60 +1,20 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from core.database import get_core_db
from . import service
from fastapi import APIRouter
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from .models import INPC
from .dto import INPCCreate, INPCResponse, INPCUpdate
from .service import INPCService
router = APIRouter(prefix="/inpc", tags=["a76.general_catalogs.inpc"])
inpc_crud = TenantCRUDRoutes(
service=INPCService,
create_schema=INPCCreate,
update_schema=INPCUpdate,
response_schema=INPCResponse,
prefix="",
tags=["INPC"],
resource_name="INPC",
enable_list=True,
)
@router.post("/", response_model=INPCResponse, status_code=status.HTTP_201_CREATED)
def create_inpc(
data: INPCCreate,
session: Session = Depends(get_core_db)
):
return service.create_inpc(session, data)
@router.get("/{id}", response_model=INPCResponse)
def get_inpc(
id: int,
session: Session = Depends(get_core_db)
):
db_obj = service.get_inpc(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="INPC not found")
return db_obj
@router.get("/", response_model=List[INPCResponse])
def get_inpcs(
skip: int = 0,
limit: int = 100,
session: Session = Depends(get_core_db)
):
return service.get_inpcs(session, skip, limit)
@router.put("/{id}", response_model=INPCResponse)
def update_inpc(
id: int,
data: INPCUpdate,
session: Session = Depends(get_core_db)
):
db_obj = service.get_inpc(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="INPC not found")
return service.update_inpc(session, db_obj, data)
@router.delete("/{id}", response_model=INPCResponse)
def delete_inpc(
id: int,
session: Session = Depends(get_core_db)
):
db_obj = service.get_inpc(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="INPC not found")
return service.delete_inpc(session, db_obj)
router.include_router(inpc_crud.router)

View File

@@ -1,39 +1,95 @@
from typing import List, Optional, Tuple, Dict, Any
from sqlalchemy.orm import Session
from sqlalchemy import select
from typing import Sequence, Optional
from .models import INPC
from .dto import INPCCreate, INPCUpdate
def create_inpc(session: Session, data: INPCCreate) -> INPC:
db_obj = INPC(**data.model_dump())
session.add(db_obj)
session.commit()
session.refresh(db_obj)
return db_obj
class INPCService:
@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[INPC], int]:
query = db.query(INPC).filter(
INPC.tenant_id == tenant_id,
INPC.company_id == company_id
)
if filters:
# Add filters here if needed
pass
def get_inpc(session: Session, id: int) -> Optional[INPC]:
return session.get(INPC, id)
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[INPC]:
return db.query(INPC).filter(
INPC.id == id,
INPC.tenant_id == tenant_id,
INPC.company_id == company_id
).first()
def get_inpcs(session: Session, skip: int = 0, limit: int = 100) -> Sequence[INPC]:
stmt = select(INPC).offset(skip).limit(limit)
result = session.execute(stmt)
return result.scalars().all()
@staticmethod
def create(
db: Session,
data: INPCCreate,
tenant_id: int,
company_id: int
) -> INPC:
db_obj = INPC(
**data.model_dump(),
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,
id: int,
data: INPCUpdate,
tenant_id: int,
company_id: int
) -> Optional[INPC]:
db_obj = INPCService.get_by_id(db, id, tenant_id, company_id)
if not db_obj:
return None
def update_inpc(session: Session, db_obj: INPC, update_data: INPCUpdate) -> INPC:
update_dict = update_data.model_dump(exclude_unset=True)
for key, value in update_dict.items():
setattr(db_obj, key, value)
session.commit()
session.refresh(db_obj)
return db_obj
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
def delete_inpc(session: Session, db_obj: INPC) -> INPC:
session.delete(db_obj)
session.commit()
return db_obj
@staticmethod
def delete(
db: Session,
id: int,
tenant_id: int,
company_id: int
) -> bool:
db_obj = INPCService.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

@@ -8,7 +8,8 @@ from core.database import Base
class Legend(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "legends"
__table_args__ = (
UniqueConstraint("code", name="uq_legend_code"),
UniqueConstraint("code", "tenant_id", "company_id",
name="uq_legend_code"),
{"schema": "a76"}
)

View File

@@ -1,60 +1,20 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from core.database import get_core_db
from . import service
from fastapi import APIRouter
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from .models import Legend
from .dto import LegendCreate, LegendResponse, LegendUpdate
from .service import LegendService
router = APIRouter(prefix="/legends", tags=["a76.general_catalogs.legends"])
legend_crud = TenantCRUDRoutes(
service=LegendService,
create_schema=LegendCreate,
update_schema=LegendUpdate,
response_schema=LegendResponse,
prefix="",
tags=["Legends"],
resource_name="Legend",
enable_list=True,
)
@router.post("/", response_model=LegendResponse, status_code=status.HTTP_201_CREATED)
def create_legend(
data: LegendCreate,
session: Session = Depends(get_core_db)
):
return service.create_legend(session, data)
@router.get("/{id}", response_model=LegendResponse)
def get_legend(
id: int,
session: Session = Depends(get_core_db)
):
db_obj = service.get_legend(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="Legend not found")
return db_obj
@router.get("/", response_model=List[LegendResponse])
def get_legends(
skip: int = 0,
limit: int = 100,
session: Session = Depends(get_core_db)
):
return service.get_legends(session, skip, limit)
@router.put("/{id}", response_model=LegendResponse)
def update_legend(
id: int,
data: LegendUpdate,
session: Session = Depends(get_core_db)
):
db_obj = service.get_legend(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="Legend not found")
return service.update_legend(session, db_obj, data)
@router.delete("/{id}", response_model=LegendResponse)
def delete_legend(
id: int,
session: Session = Depends(get_core_db)
):
db_obj = service.get_legend(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="Legend not found")
return service.delete_legend(session, db_obj)
router.include_router(legend_crud.router)

View File

@@ -1,39 +1,95 @@
from typing import List, Optional, Tuple, Dict, Any
from sqlalchemy.orm import Session
from sqlalchemy import select
from typing import Sequence, Optional
from .models import Legend
from .dto import LegendCreate, LegendUpdate
def create_legend(session: Session, data: LegendCreate) -> Legend:
db_obj = Legend(**data.model_dump())
session.add(db_obj)
session.commit()
session.refresh(db_obj)
return db_obj
class LegendService:
@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[Legend], int]:
query = db.query(Legend).filter(
Legend.tenant_id == tenant_id,
Legend.company_id == company_id
)
if filters:
# Add filters here if needed
pass
def get_legend(session: Session, id: int) -> Optional[Legend]:
return session.get(Legend, id)
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[Legend]:
return db.query(Legend).filter(
Legend.id == id,
Legend.tenant_id == tenant_id,
Legend.company_id == company_id
).first()
def get_legends(session: Session, skip: int = 0, limit: int = 100) -> Sequence[Legend]:
stmt = select(Legend).offset(skip).limit(limit)
result = session.execute(stmt)
return result.scalars().all()
@staticmethod
def create(
db: Session,
data: LegendCreate,
tenant_id: int,
company_id: int
) -> Legend:
db_obj = Legend(
**data.model_dump(),
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,
id: int,
data: LegendUpdate,
tenant_id: int,
company_id: int
) -> Optional[Legend]:
db_obj = LegendService.get_by_id(db, id, tenant_id, company_id)
if not db_obj:
return None
def update_legend(session: Session, db_obj: Legend, update_data: LegendUpdate) -> Legend:
update_dict = update_data.model_dump(exclude_unset=True)
for key, value in update_dict.items():
setattr(db_obj, key, value)
session.commit()
session.refresh(db_obj)
return db_obj
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
def delete_legend(session: Session, db_obj: Legend) -> Legend:
session.delete(db_obj)
session.commit()
return db_obj
@staticmethod
def delete(
db: Session,
id: int,
tenant_id: int,
company_id: int
) -> bool:
db_obj = LegendService.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

@@ -11,7 +11,7 @@ from api.v1.modules.public.reference_data.countries.models import Country
class MultiCurrencyType(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "multi_currency_types"
__table_args__ = (
UniqueConstraint("currency_type_code", "publication_date",
UniqueConstraint("currency_type_code", "publication_date", "tenant_id", "company_id",
name="uq_multi_currency_type_code_date"),
{"schema": "a76"}
)

View File

@@ -1,64 +1,21 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from core.database import get_core_db
from . import service
from fastapi import APIRouter
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from .models import MultiCurrencyType
from .dto import MultiCurrencyTypeCreate, MultiCurrencyTypeResponse, MultiCurrencyTypeUpdate
from .service import MultiCurrencyTypeService
router = APIRouter(prefix="/multi-currency-types",
tags=["a76.general_catalogs.multi_currency_types"])
multi_currency_type_crud = TenantCRUDRoutes(
service=MultiCurrencyTypeService,
create_schema=MultiCurrencyTypeCreate,
update_schema=MultiCurrencyTypeUpdate,
response_schema=MultiCurrencyTypeResponse,
prefix="",
tags=["Multi Currency Types"],
resource_name="MultiCurrencyType",
enable_list=True,
)
@router.post("/", response_model=MultiCurrencyTypeResponse, status_code=status.HTTP_201_CREATED)
def create_multi_currency_type(
data: MultiCurrencyTypeCreate,
session: Session = Depends(get_core_db)
):
return service.create_multi_currency_type(session, data)
@router.get("/{id}", response_model=MultiCurrencyTypeResponse)
def get_multi_currency_type(
id: int,
session: Session = Depends(get_core_db)
):
db_obj = service.get_multi_currency_type(session, id)
if not db_obj:
raise HTTPException(
status_code=404, detail="MultiCurrencyType not found")
return db_obj
@router.get("/", response_model=List[MultiCurrencyTypeResponse])
def get_multi_currency_types(
skip: int = 0,
limit: int = 100,
session: Session = Depends(get_core_db)
):
return service.get_multi_currency_types(session, skip, limit)
@router.put("/{id}", response_model=MultiCurrencyTypeResponse)
def update_multi_currency_type(
id: int,
data: MultiCurrencyTypeUpdate,
session: Session = Depends(get_core_db)
):
db_obj = service.get_multi_currency_type(session, id)
if not db_obj:
raise HTTPException(
status_code=404, detail="MultiCurrencyType not found")
return service.update_multi_currency_type(session, db_obj, data)
@router.delete("/{id}", response_model=MultiCurrencyTypeResponse)
def delete_multi_currency_type(
id: int,
session: Session = Depends(get_core_db)
):
db_obj = service.get_multi_currency_type(session, id)
if not db_obj:
raise HTTPException(
status_code=404, detail="MultiCurrencyType not found")
return service.delete_multi_currency_type(session, db_obj)
router.include_router(multi_currency_type_crud.router)

View File

@@ -1,39 +1,97 @@
from typing import List, Optional, Tuple, Dict, Any
from sqlalchemy.orm import Session
from sqlalchemy import select
from typing import Sequence, Optional
from .models import MultiCurrencyType
from .dto import MultiCurrencyTypeCreate, MultiCurrencyTypeUpdate
def create_multi_currency_type(session: Session, data: MultiCurrencyTypeCreate) -> MultiCurrencyType:
db_obj = MultiCurrencyType(**data.model_dump())
session.add(db_obj)
session.commit()
session.refresh(db_obj)
return db_obj
class MultiCurrencyTypeService:
@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[MultiCurrencyType], int]:
query = db.query(MultiCurrencyType).filter(
MultiCurrencyType.tenant_id == tenant_id,
MultiCurrencyType.company_id == company_id
)
if filters:
# Add filters here if needed
pass
def get_multi_currency_type(session: Session, id: int) -> Optional[MultiCurrencyType]:
return session.get(MultiCurrencyType, id)
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[MultiCurrencyType]:
return db.query(MultiCurrencyType).filter(
MultiCurrencyType.id == id,
MultiCurrencyType.tenant_id == tenant_id,
MultiCurrencyType.company_id == company_id
).first()
def get_multi_currency_types(session: Session, skip: int = 0, limit: int = 100) -> Sequence[MultiCurrencyType]:
stmt = select(MultiCurrencyType).offset(skip).limit(limit)
result = session.execute(stmt)
return result.scalars().all()
@staticmethod
def create(
db: Session,
data: MultiCurrencyTypeCreate,
tenant_id: int,
company_id: int
) -> MultiCurrencyType:
db_obj = MultiCurrencyType(
**data.model_dump(),
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,
id: int,
data: MultiCurrencyTypeUpdate,
tenant_id: int,
company_id: int
) -> Optional[MultiCurrencyType]:
db_obj = MultiCurrencyTypeService.get_by_id(
db, id, tenant_id, company_id)
if not db_obj:
return None
def update_multi_currency_type(session: Session, db_obj: MultiCurrencyType, update_data: MultiCurrencyTypeUpdate) -> MultiCurrencyType:
update_dict = update_data.model_dump(exclude_unset=True)
for key, value in update_dict.items():
setattr(db_obj, key, value)
session.commit()
session.refresh(db_obj)
return db_obj
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
def delete_multi_currency_type(session: Session, db_obj: MultiCurrencyType) -> MultiCurrencyType:
session.delete(db_obj)
session.commit()
return db_obj
@staticmethod
def delete(
db: Session,
id: int,
tenant_id: int,
company_id: int
) -> bool:
db_obj = MultiCurrencyTypeService.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

@@ -35,7 +35,8 @@ class PackageService:
)
if filters.get("description_es"):
query = query.filter(
models.Package.description_es.ilike(f"%{filters['description_es']}%")
models.Package.description_es.ilike(
f"%{filters['description_es']}%")
)
total = query.count()
@@ -79,16 +80,18 @@ class PackageService:
db: Session,
package_id: int,
tenant_id: int,
company_id: int,
package_data: dto.PackageUpdateDTO,
company_id: int,
) -> Optional[models.Package]:
"""Update a package"""
package = PackageService.get_by_id(db, package_id, tenant_id, company_id)
package = PackageService.get_by_id(
db, package_id, tenant_id, company_id)
if not package:
return None
# Update fields (excluding key if it's meant to be immutable)
update_data = package_data.model_dump(exclude_unset=True, exclude={"key"})
update_data = package_data.model_dump(
exclude_unset=True, exclude={"key"})
for field, value in update_data.items():
setattr(package, field, value)
@@ -101,7 +104,8 @@ class PackageService:
db: Session, package_id: int, tenant_id: int, company_id: int
) -> bool:
"""Delete a package"""
package = PackageService.get_by_id(db, package_id, tenant_id, company_id)
package = PackageService.get_by_id(
db, package_id, tenant_id, company_id)
if not package:
return False

View File

@@ -15,7 +15,7 @@ class PortType(str, enum.Enum):
class Port(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "ports"
__table_args__ = (
UniqueConstraint("port_code", "location_code",
UniqueConstraint("port_code", "location_code", "tenant_id", "company_id",
name="uq_port_location"),
{"schema": "a76"}
)

View File

@@ -1,60 +1,16 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from core.database import get_core_db
from . import service
from fastapi import APIRouter
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from .models import Port
from .dto import PortCreate, PortResponse, PortUpdate
from .service import PortService
router = APIRouter(prefix="/ports", tags=["a76.general_catalogs.ports"])
@router.post("/", response_model=PortResponse, status_code=status.HTTP_201_CREATED)
def create_port(
data: PortCreate,
session: Session = Depends(get_core_db)
):
return service.create_port(session, data)
@router.get("/{id}", response_model=PortResponse)
def get_port(
id: int,
session: Session = Depends(get_core_db)
):
db_obj = service.get_port(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="Port not found")
return db_obj
@router.get("/", response_model=List[PortResponse])
def get_ports(
skip: int = 0,
limit: int = 100,
session: Session = Depends(get_core_db)
):
return service.get_ports(session, skip, limit)
@router.put("/{id}", response_model=PortResponse)
def update_port(
id: int,
data: PortUpdate,
session: Session = Depends(get_core_db)
):
db_obj = service.get_port(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="Port not found")
return service.update_port(session, db_obj, data)
@router.delete("/{id}", response_model=PortResponse)
def delete_port(
id: int,
session: Session = Depends(get_core_db)
):
db_obj = service.get_port(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="Port not found")
return service.delete_port(session, db_obj)
router = TenantCRUDRoutes(
service=PortService,
create_schema=PortCreate,
update_schema=PortUpdate,
response_schema=PortResponse,
prefix="/ports",
tags=["a76.general_catalogs.ports"],
resource_name="Port",
enable_list=True,
).router

View File

@@ -1,38 +1,95 @@
from typing import List, Optional, Tuple, Dict, Any
from sqlalchemy.orm import Session
from sqlalchemy import select
from typing import Sequence, Optional
from .models import Port
from .dto import PortCreate, PortUpdate
def create_port(session: Session, data: PortCreate) -> Port:
db_obj = Port(**data.model_dump())
session.add(db_obj)
session.commit()
session.refresh(db_obj)
return db_obj
class PortService:
@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[Port], int]:
query = db.query(Port).filter(
Port.tenant_id == tenant_id,
Port.company_id == company_id
)
if filters:
# Add filters here if needed
pass
def get_port(session: Session, id: int) -> Optional[Port]:
return session.get(Port, id)
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[Port]:
return db.query(Port).filter(
Port.id == id,
Port.tenant_id == tenant_id,
Port.company_id == company_id
).first()
def get_ports(session: Session, skip: int = 0, limit: int = 100) -> Sequence[Port]:
stmt = select(Port).offset(skip).limit(limit)
result = session.execute(stmt)
return result.scalars().all()
@staticmethod
def create(
db: Session,
data: PortCreate,
tenant_id: int,
company_id: int
) -> Port:
db_obj = Port(
**data.model_dump(),
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,
id: int,
data: PortUpdate,
tenant_id: int,
company_id: int
) -> Optional[Port]:
db_obj = PortService.get_by_id(db, id, tenant_id, company_id)
if not db_obj:
return None
def update_port(session: Session, db_obj: Port, update_data: PortUpdate) -> Port:
update_dict = update_data.model_dump(exclude_unset=True)
for key, value in update_dict.items():
setattr(db_obj, key, value)
session.commit()
session.refresh(db_obj)
return db_obj
update_dict = data.model_dump(exclude_unset=True)
for key, value in update_dict.items():
setattr(db_obj, key, value)
def delete_port(session: Session, db_obj: Port) -> Port:
session.delete(db_obj)
session.commit()
return db_obj
db.commit()
db.refresh(db_obj)
return db_obj
@staticmethod
def delete(
db: Session,
id: int,
tenant_id: int,
company_id: int
) -> bool:
db_obj = PortService.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

@@ -18,7 +18,8 @@ class Prevalidator(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "prevalidators" # GPrevalidadores
__table_args__ = (
PrimaryKeyConstraint("id", name="prevalidators_pkey"),
UniqueConstraint("code", name="prevalidators_code_unique"),
UniqueConstraint("code", "tenant_id", "company_id",
name="prevalidators_code_unique"),
{"schema": "a76"},
)
@@ -26,7 +27,7 @@ class Prevalidator(Base, TenantScopedMixin, TimestampMixin):
id: Mapped[int] = mapped_column(Integer, primary_key=True)
# Prevalidator code (unique)
code: Mapped[str] = mapped_column(String(20), nullable=False, unique=True)
code: Mapped[str] = mapped_column(String(20), nullable=False)
# Prevalidator information
customs_prevalidator: Mapped[Optional[str]] = mapped_column(String(20))

View File

@@ -4,10 +4,12 @@ Rutas para gestión de prevalidadores
from typing import List
from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from core.database import get_core_db
from core.security import get_current_user
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from .dto import (
PrevalidatorCreateDTO,
PrevalidatorResponseDTO,
@@ -18,63 +20,21 @@ from .service import PrevalidatorService
router = APIRouter(prefix="/prevalidators", tags=["prevalidators"])
@router.get(
"",
response_model=dict,
summary="Get all prevalidators",
prevalidator_crud = TenantCRUDRoutes(
service=PrevalidatorService,
create_schema=PrevalidatorCreateDTO,
update_schema=PrevalidatorUpdateDTO,
response_schema=PrevalidatorResponseDTO,
prefix="",
tags=["Prevalidators"],
resource_name="Prevalidator",
enable_list=True,
)
async def get_all_prevalidators(
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
code: str = Query(None),
description: str = Query(None),
customs_prevalidator: str = Query(None),
db: Session = Depends(get_core_db),
):
"""Get all prevalidators with optional filtering and pagination"""
filters = {}
if code:
filters["code"] = code
if description:
filters["description"] = description
if customs_prevalidator:
filters["customs_prevalidator"] = customs_prevalidator
prevalidators, total = PrevalidatorService.get_all(
db, skip, limit, filters)
return {
"data": [
PrevalidatorResponseDTO.model_validate(prevalidator)
for prevalidator in prevalidators
],
"total": total,
"skip": skip,
"limit": limit,
}
# Add custom endpoints
@router.get(
"/{prevalidator_id}",
response_model=PrevalidatorResponseDTO,
summary="Get prevalidator by ID",
)
async def get_prevalidator(
prevalidator_id: int,
db: Session = Depends(get_core_db),
):
"""Get a prevalidator by its ID"""
prevalidator = PrevalidatorService.get_by_id(db, prevalidator_id)
if not prevalidator:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Prevalidator not found",
)
return PrevalidatorResponseDTO.model_validate(prevalidator)
@router.get(
@prevalidator_crud.router.get(
"/code/{code}",
response_model=PrevalidatorResponseDTO,
summary="Get prevalidator by code",
@@ -82,9 +42,15 @@ async def get_prevalidator(
async def get_prevalidator_by_code(
code: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Get a prevalidator by its code"""
prevalidator = PrevalidatorService.get_by_code(db, code)
prevalidator = PrevalidatorService.get_by_code(
db,
code,
tenant_id=current_user["tenant_id"],
company_id=current_user["company_id"]
)
if not prevalidator:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -92,13 +58,9 @@ async def get_prevalidator_by_code(
)
return PrevalidatorResponseDTO.model_validate(prevalidator)
router.include_router(prevalidator_crud.router)
@router.post(
"",
response_model=PrevalidatorResponseDTO,
status_code=status.HTTP_201_CREATED,
summary="Create prevalidator",
)
async def create_prevalidator(
prevalidator_data: PrevalidatorCreateDTO,
db: Session = Depends(get_core_db),

View File

@@ -22,18 +22,20 @@ logger = logging.getLogger(__name__)
class PrevalidatorService:
"""Servicio para gestión de prevalidadores"""
def __init__(self, db: Session):
self.db = db
@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[Prevalidator], int]:
"""Get all prevalidators with pagination"""
query = db.query(Prevalidator)
query = db.query(Prevalidator).filter(
Prevalidator.tenant_id == tenant_id,
Prevalidator.company_id == company_id
)
# Apply filters if provided
if filters:
@@ -59,21 +61,40 @@ class PrevalidatorService:
return prevalidators, total
@staticmethod
def get_by_code(db: Session, code: str) -> Optional[Prevalidator]:
def get_by_code(
db: Session, code: str, tenant_id: int, company_id: int
) -> Optional[Prevalidator]:
"""Get prevalidator by code"""
return db.query(Prevalidator).filter(Prevalidator.code == code).first()
return db.query(Prevalidator).filter(
Prevalidator.code == code,
Prevalidator.tenant_id == tenant_id,
Prevalidator.company_id == company_id
).first()
@staticmethod
def get_by_id(db: Session, prevalidator_id: int) -> Optional[Prevalidator]:
def get_by_id(
db: Session, prevalidator_id: int, tenant_id: int, company_id: int
) -> Optional[Prevalidator]:
"""Get prevalidator by ID"""
return db.query(Prevalidator).filter(Prevalidator.id == prevalidator_id).first()
return db.query(Prevalidator).filter(
Prevalidator.id == prevalidator_id,
Prevalidator.tenant_id == tenant_id,
Prevalidator.company_id == company_id
).first()
@staticmethod
def create(db: Session, prevalidator_data: PrevalidatorCreateDTO) -> Prevalidator:
def create(
db: Session,
prevalidator_data: PrevalidatorCreateDTO,
tenant_id: int,
company_id: int
) -> Prevalidator:
"""Create a new prevalidator"""
try:
db_prevalidator = Prevalidator(
**prevalidator_data.model_dump(exclude_unset=True)
**prevalidator_data.model_dump(exclude_unset=True),
tenant_id=tenant_id,
company_id=company_id
)
db.add(db_prevalidator)
@@ -100,13 +121,14 @@ class PrevalidatorService:
db: Session,
prevalidator_id: int,
prevalidator_data: PrevalidatorUpdateDTO,
tenant_id: int,
company_id: int
) -> Optional[Prevalidator]:
"""Update a prevalidator"""
try:
db_prevalidator = db.query(Prevalidator).filter(
Prevalidator.id == prevalidator_id
).first()
db_prevalidator = PrevalidatorService.get_by_id(
db, prevalidator_id, tenant_id, company_id
)
if not db_prevalidator:
return None
@@ -133,13 +155,14 @@ class PrevalidatorService:
)
@staticmethod
def delete(db: Session, prevalidator_id: int) -> bool:
def delete(
db: Session, prevalidator_id: int, tenant_id: int, company_id: int
) -> bool:
"""Delete a prevalidator"""
try:
db_prevalidator = db.query(Prevalidator).filter(
Prevalidator.id == prevalidator_id
).first()
db_prevalidator = PrevalidatorService.get_by_id(
db, prevalidator_id, tenant_id, company_id
)
if not db_prevalidator:
return False
@@ -155,10 +178,16 @@ class PrevalidatorService:
status_code=500, detail="Error deleting prevalidator")
@staticmethod
def get_by_customs(db: Session, customs: str) -> List[Prevalidator]:
def get_by_customs(
db: Session, customs: str, tenant_id: int, company_id: int
) -> List[Prevalidator]:
"""Get all prevalidators by customs"""
return (
db.query(Prevalidator)
.filter(Prevalidator.customs_prevalidator == customs)
.filter(
Prevalidator.customs_prevalidator == customs,
Prevalidator.tenant_id == tenant_id,
Prevalidator.company_id == company_id
)
.all()
)

View File

@@ -1,7 +1,6 @@
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
ForeignKeyConstraint,
Integer,
PrimaryKeyConstraint,
String,
@@ -13,7 +12,7 @@ from sqlalchemy.orm import Mapped, mapped_column
class Seal(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "seal"
__table_args__ = (
PrimaryKeyConstraint("id", name="seal_pkey"),
PrimaryKeyConstraint("id", name="seal_pkey"),
UniqueConstraint("tenant_id", "company_id", "seal", name="seal_ukey"),
{"schema": "a76"},
)

View File

@@ -18,7 +18,8 @@ class Signature(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "signatures" # GFirmas
__table_args__ = (
PrimaryKeyConstraint("id", name="signatures_pkey"),
UniqueConstraint("code", name="signatures_code_unique"),
UniqueConstraint("code", "tenant_id", "company_id",
name="signatures_code_unique"),
{"schema": "a76"},
)
@@ -27,7 +28,7 @@ class Signature(Base, TenantScopedMixin, TimestampMixin):
Integer, primary_key=True, autoincrement=True)
# Signature code (unique)
code: Mapped[str] = mapped_column(String(10), nullable=False, unique=True)
code: Mapped[str] = mapped_column(String(10), nullable=False)
# Signature information
signature: Mapped[Optional[str]] = mapped_column(String(1000))

View File

@@ -2,60 +2,34 @@
Rutas para gestión de firmas
"""
from fastapi import APIRouter, Depends, HTTPException, Query, status
from typing import Any, Dict
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes, validate_access_to_resource
from fastapi import Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from core.database import get_core_db
from .dto import SignatureCreateDTO, SignatureResponseDTO, SignatureUpdateDTO
from .models import Signature
from .service import SignatureService
router = APIRouter(prefix="/signatures", tags=["signatures"])
@router.get(
"",
response_model=dict,
summary="Get all signatures",
# Create router using TenantCRUDRoutes factory
signature_crud = TenantCRUDRoutes(
service=SignatureService,
create_schema=SignatureCreateDTO,
update_schema=SignatureUpdateDTO,
response_schema=SignatureResponseDTO,
prefix="/signatures",
tags=["signatures"],
resource_name="Signature",
id_name="id", # Using numeric ID
enable_list=True, # Enable GET /signatures with pagination
enable_filters=True, # Enable filtering by code
default_page_size=50,
max_page_size=100,
)
async def get_all_signatures(
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
code: str = Query(None),
db: Session = Depends(get_core_db),
):
"""Get all signatures with optional filtering and pagination"""
filters = {}
if code:
filters["code"] = code
signatures, total = SignatureService.get_all(db, skip, limit, filters)
return {
"data": [SignatureResponseDTO.model_validate(sig) for sig in signatures],
"total": total,
"skip": skip,
"limit": limit,
}
@router.get(
"/{signature_id}",
response_model=SignatureResponseDTO,
summary="Get signature by ID",
)
async def get_signature(
signature_id: int,
db: Session = Depends(get_core_db),
):
"""Get a signature by its ID"""
signature = SignatureService.get_by_id(db, signature_id)
if not signature:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Signature not found",
)
return SignatureResponseDTO.model_validate(signature)
router = signature_crud.router
@router.get(
@@ -65,48 +39,16 @@ async def get_signature(
)
async def get_signature_by_code(
code: str,
db: Session = Depends(get_core_db),
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(signature_crud.db_dependency),
current_user: Dict[str, Any] = Depends(signature_crud.auth_dependency),
):
"""Get a signature by its code"""
signature = SignatureService.get_by_code(db, code)
tenant_id = validate_access_to_resource(db, company_id, current_user)
signature = SignatureService.get_by_code(db, code, tenant_id, company_id)
if not signature:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Signature not found",
)
return SignatureResponseDTO.model_validate(signature)
@router.post(
"",
response_model=SignatureResponseDTO,
status_code=status.HTTP_201_CREATED,
summary="Create signature",
)
async def create_signature(
signature_data: SignatureCreateDTO,
db: Session = Depends(get_core_db),
):
"""Create a new signature"""
signature = SignatureService.create(db, signature_data)
return SignatureResponseDTO.model_validate(signature)
@router.put(
"/{signature_id}",
response_model=SignatureResponseDTO,
summary="Update signature",
)
async def update_signature(
signature_id: int,
signature_data: SignatureUpdateDTO,
db: Session = Depends(get_core_db),
):
"""Update a signature"""
signature = SignatureService.update(db, signature_id, signature_data)
if not signature:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
status_code=404,
detail="Signature not found",
)
return SignatureResponseDTO.model_validate(signature)

View File

@@ -5,12 +5,9 @@ Capa de servicio para lógica de negocio de firmas
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 .dto import SignatureCreateDTO, SignatureResponseDTO, SignatureUpdateDTO
from .models import Signature
from . import dto, models
logger = logging.getLogger(__name__)
@@ -18,23 +15,25 @@ logger = logging.getLogger(__name__)
class SignatureService:
"""Servicio para gestión de firmas"""
def __init__(self, db: Session):
self.db = db
@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[Signature], int]:
) -> Tuple[List[models.Signature], int]:
"""Get all signatures with pagination"""
query = db.query(Signature)
query = db.query(models.Signature).filter(
models.Signature.tenant_id == tenant_id,
models.Signature.company_id == company_id,
)
if filters:
if filters.get("code"):
query = query.filter(
Signature.code.ilike(f"%{filters['code']}%"))
models.Signature.code.ilike(f"%{filters['code']}%"))
total = query.count()
signatures = query.offset(skip).limit(limit).all()
@@ -42,93 +41,82 @@ class SignatureService:
return signatures, total
@staticmethod
def get_by_id(db: Session, signature_id: int) -> Optional[Signature]:
def get_by_id(
db: Session, signature_id: int, tenant_id: int, company_id: int
) -> Optional[models.Signature]:
"""Get signature by ID"""
return db.query(Signature).filter(Signature.id == signature_id).first()
@staticmethod
def get_by_code(db: Session, code: str) -> Optional[Signature]:
"""Get signature by code"""
return db.query(Signature).filter(Signature.code == code).first()
@staticmethod
def create(db: Session, signature_data: SignatureCreateDTO) -> Signature:
"""Create a new signature"""
try:
db_signature = Signature(
**signature_data.model_dump(exclude_unset=True))
db.add(db_signature)
db.commit()
db.refresh(db_signature)
return db_signature
except IntegrityError as e:
db.rollback()
logger.error(f"IntegrityError creating signature: {str(e)}")
raise HTTPException(
status_code=400,
detail="Signature code already exists",
return (
db.query(models.Signature)
.filter(
models.Signature.id == signature_id,
models.Signature.tenant_id == tenant_id,
models.Signature.company_id == company_id,
)
except Exception as e:
db.rollback()
logger.error(f"Error creating signature: {str(e)}")
raise HTTPException(
status_code=500, detail="Error creating signature")
.first()
)
@staticmethod
def get_by_code(
db: Session, code: str, tenant_id: int, company_id: int
) -> Optional[models.Signature]:
"""Get signature by code"""
return (
db.query(models.Signature)
.filter(
models.Signature.code == code,
models.Signature.tenant_id == tenant_id,
models.Signature.company_id == company_id,
)
.first()
)
@staticmethod
def create(
db: Session,
signature_data: dto.SignatureCreateDTO,
tenant_id: int,
company_id: int,
) -> models.Signature:
"""Create a new signature"""
new_signature = models.Signature(
**signature_data.model_dump(), tenant_id=tenant_id, company_id=company_id
)
db.add(new_signature)
db.commit()
db.refresh(new_signature)
return new_signature
@staticmethod
def update(
db: Session, signature_id: int, signature_data: SignatureUpdateDTO
) -> Optional[Signature]:
db: Session,
signature_id: int,
tenant_id: int,
signature_data: dto.SignatureUpdateDTO,
company_id: int,
) -> Optional[models.Signature]:
"""Update a signature"""
try:
db_signature = db.query(Signature).filter(
Signature.id == signature_id
).first()
signature = SignatureService.get_by_id(
db, signature_id, tenant_id, company_id)
if not signature:
return None
if not db_signature:
return None
for key, value in signature_data.model_dump(exclude_unset=True).items():
setattr(signature, key, value)
for key, value in signature_data.model_dump(exclude_unset=True).items():
setattr(db_signature, key, value)
db.commit()
db.refresh(db_signature)
return db_signature
except IntegrityError as e:
db.rollback()
logger.error(f"IntegrityError updating signature: {str(e)}")
raise HTTPException(
status_code=400,
detail="Error updating signature",
)
except Exception as e:
db.rollback()
logger.error(f"Error updating signature: {str(e)}")
raise HTTPException(
status_code=500, detail="Error updating signature")
db.commit()
db.refresh(signature)
return signature
@staticmethod
def delete(db: Session, signature_id: int) -> bool:
def delete(
db: Session, signature_id: int, tenant_id: int, company_id: int
) -> bool:
"""Delete a signature"""
try:
db_signature = db.query(Signature).filter(
Signature.id == signature_id
).first()
signature = SignatureService.get_by_id(
db, signature_id, tenant_id, company_id)
if not signature:
return False
if not db_signature:
return False
db.delete(db_signature)
db.commit()
return True
except Exception as e:
db.rollback()
logger.error(f"Error deleting signature: {str(e)}")
raise HTTPException(
status_code=500, detail="Error deleting signature")
db.delete(signature)
db.commit()
return True

View File

@@ -1,6 +1,6 @@
from typing import Optional
from decimal import Decimal
from sqlalchemy import Integer, String, ForeignKey, UniqueConstraint, Numeric
from sqlalchemy import Integer, String, ForeignKeyConstraint, UniqueConstraint, Numeric
from sqlalchemy.orm import Mapped, mapped_column, relationship
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
@@ -10,21 +10,27 @@ from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMe
class UnitConversion(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "unit_conversions"
__table_args__ = (
UniqueConstraint("from_unit_code", "to_unit_code",
UniqueConstraint("from_unit_code", "to_unit_code", "tenant_id", "company_id",
name="uq_unit_conversion_pair"),
ForeignKeyConstraint(
["from_unit_code", "tenant_id", "company_id"],
["a76.units_of_measure.code", "a76.units_of_measure.tenant_id",
"a76.units_of_measure.company_id"],
),
ForeignKeyConstraint(
["to_unit_code", "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)
from_unit_code: Mapped[str] = mapped_column(
String(5), ForeignKey("a76.units_of_measure.code"), nullable=False)
to_unit_code: Mapped[str] = mapped_column(
String(5), ForeignKey("a76.units_of_measure.code"), nullable=False)
from_unit_code: Mapped[str] = mapped_column(String(5), nullable=False)
to_unit_code: Mapped[str] = mapped_column(String(5), nullable=False)
conversion_factor: Mapped[Optional[Decimal]] = mapped_column(
Numeric(13, 6), nullable=True)
from_unit: Mapped["UnitOfMeasure"] = relationship(
foreign_keys=[from_unit_code])
to_unit: Mapped["UnitOfMeasure"] = relationship(
foreign_keys=[to_unit_code])
# Relationships
# Note: Complex composite foreign keys might require explicit primaryjoin if used

View File

@@ -1,61 +1,16 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from core.database import get_core_db
from . import service
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from .dto import UnitConversionCreate, UnitConversionResponse, UnitConversionUpdate
from .service import UnitConversionService
router = APIRouter(prefix="/unit-conversions",
tags=["a76.general_catalogs.unit_conversions"])
@router.post("/", response_model=UnitConversionResponse, status_code=status.HTTP_201_CREATED)
def create_unit_conversion(
data: UnitConversionCreate,
session: Session = Depends(get_core_db)
):
return service.create_unit_conversion(session, data)
@router.get("/{id}", response_model=UnitConversionResponse)
def get_unit_conversion(
id: int,
session: Session = Depends(get_core_db)
):
db_obj = service.get_unit_conversion(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="UnitConversion not found")
return db_obj
@router.get("/", response_model=List[UnitConversionResponse])
def get_unit_conversions(
skip: int = 0,
limit: int = 100,
session: Session = Depends(get_core_db)
):
return service.get_unit_conversions(session, skip, limit)
@router.put("/{id}", response_model=UnitConversionResponse)
def update_unit_conversion(
id: int,
data: UnitConversionUpdate,
session: Session = Depends(get_core_db)
):
db_obj = service.get_unit_conversion(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="UnitConversion not found")
return service.update_unit_conversion(session, db_obj, data)
@router.delete("/{id}", response_model=UnitConversionResponse)
def delete_unit_conversion(
id: int,
session: Session = Depends(get_core_db)
):
db_obj = service.get_unit_conversion(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="UnitConversion not found")
return service.delete_unit_conversion(session, db_obj)
router = TenantCRUDRoutes(
service=UnitConversionService,
create_schema=UnitConversionCreate,
update_schema=UnitConversionUpdate,
response_schema=UnitConversionResponse,
prefix="/unit-conversions",
tags=["a76.general_catalogs.unit_conversions"],
resource_name="UnitConversion",
id_name="id",
enable_list=True,
enable_filters=True,
).router

View File

@@ -1,39 +1,89 @@
from typing import List, Optional, Tuple, Dict, Any
from sqlalchemy.orm import Session
from sqlalchemy import select
from typing import Sequence, Optional
from .models import UnitConversion
from .dto import UnitConversionCreate, UnitConversionUpdate
def create_unit_conversion(session: Session, data: UnitConversionCreate) -> UnitConversion:
db_obj = UnitConversion(**data.model_dump())
session.add(db_obj)
session.commit()
session.refresh(db_obj)
return db_obj
class UnitConversionService:
@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[UnitConversion], int]:
query = db.query(UnitConversion).filter(
UnitConversion.tenant_id == tenant_id,
UnitConversion.company_id == company_id,
)
if filters:
# Add filters if needed
pass
def get_unit_conversion(session: Session, id: int) -> Optional[UnitConversion]:
return session.get(UnitConversion, id)
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[UnitConversion]:
return db.query(UnitConversion).filter(
UnitConversion.id == id,
UnitConversion.tenant_id == tenant_id,
UnitConversion.company_id == company_id,
).first()
def get_unit_conversions(session: Session, skip: int = 0, limit: int = 100) -> Sequence[UnitConversion]:
stmt = select(UnitConversion).offset(skip).limit(limit)
result = session.execute(stmt)
return result.scalars().all()
@staticmethod
def create(
db: Session,
data: UnitConversionCreate,
tenant_id: int,
company_id: int,
) -> UnitConversion:
db_obj = UnitConversion(
**data.model_dump(),
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,
id: int,
tenant_id: int,
data: UnitConversionUpdate,
company_id: int,
) -> Optional[UnitConversion]:
db_obj = UnitConversionService.get_by_id(db, id, tenant_id, company_id)
if not db_obj:
return None
def update_unit_conversion(session: Session, db_obj: UnitConversion, update_data: UnitConversionUpdate) -> UnitConversion:
update_dict = update_data.model_dump(exclude_unset=True)
for key, value in update_dict.items():
setattr(db_obj, key, value)
session.commit()
session.refresh(db_obj)
return db_obj
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
def delete_unit_conversion(session: Session, db_obj: UnitConversion) -> UnitConversion:
session.delete(db_obj)
session.commit()
return db_obj
@staticmethod
def delete(
db: Session, id: int, tenant_id: int, company_id: int
) -> bool:
db_obj = UnitConversionService.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

@@ -1,16 +1,19 @@
from typing import Optional
from decimal import Decimal
from sqlalchemy import Integer, String, ForeignKey, UniqueConstraint, Numeric
from sqlalchemy import ForeignKey, Integer, String, ForeignKeyConstraint, UniqueConstraint, Numeric
from sqlalchemy.orm import Mapped, mapped_column, relationship
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
# 1. GUniMedACE
class UnitOfMeasureACE(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "unit_of_measure_ace"
__table_args__ = (
UniqueConstraint("code", name="uq_uom_ace_code"),
{"schema": "a76"}
UniqueConstraint("code", "tenant_id", "company_id",
name="uq_uom_ace_code"),
{"schema": "a76", "extend_existing": True}
)
id: Mapped[int] = mapped_column(
@@ -20,11 +23,14 @@ class UnitOfMeasureACE(Base, TenantScopedMixin, TimestampMixin):
String(49), nullable=True)
# 2. GUMOMA
class UnitOfMeasureOMA(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "unit_of_measure_oma"
__table_args__ = (
UniqueConstraint("code", name="uq_uom_oma_code"),
{"schema": "a76"}
UniqueConstraint("code", "tenant_id", "company_id",
name="uq_uom_oma_code"),
{"schema": "a76", "extend_existing": True}
)
id: Mapped[int] = mapped_column(
@@ -34,11 +40,14 @@ class UnitOfMeasureOMA(Base, TenantScopedMixin, TimestampMixin):
String(200), nullable=True)
# 3. GUMAme
class UnitOfMeasureAmerican(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "unit_of_measure_american"
__table_args__ = (
UniqueConstraint("code", name="uq_uom_american_code"),
{"schema": "a76"}
UniqueConstraint("code", "tenant_id", "company_id",
name="uq_uom_american_code"),
{"schema": "a76", "extend_existing": True}
)
id: Mapped[int] = mapped_column(
@@ -48,11 +57,14 @@ class UnitOfMeasureAmerican(Base, TenantScopedMixin, TimestampMixin):
String(40), nullable=True)
# 4. GUMAduana
class UnitOfMeasureCustoms(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "unit_of_measure_customs"
__table_args__ = (
UniqueConstraint("code", name="uq_uom_customs_code"),
{"schema": "a76"}
UniqueConstraint("code", "tenant_id", "company_id",
name="uq_uom_customs_code"),
{"schema": "a76", "extend_existing": True}
)
id: Mapped[int] = mapped_column(
@@ -64,11 +76,42 @@ class UnitOfMeasureCustoms(Base, TenantScopedMixin, TimestampMixin):
String(5), nullable=True) # UNIDADSCAII
# 5. GUniMedida (Main)
class UnitOfMeasure(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "units_of_measure"
__table_args__ = (
UniqueConstraint("code", name="uq_uom_code"),
{"schema": "a76"}
UniqueConstraint("code", "tenant_id",
"company_id", name="uq_uom_code"),
ForeignKeyConstraint(
["customs_code", "tenant_id", "company_id"],
["a76.unit_of_measure_customs.code", "a76.unit_of_measure_customs.tenant_id",
"a76.unit_of_measure_customs.company_id"],
use_alter=True,
name="fk_uom_customs"
),
ForeignKeyConstraint(
["american_code", "tenant_id", "company_id"],
["a76.unit_of_measure_american.code", "a76.unit_of_measure_american.tenant_id",
"a76.unit_of_measure_american.company_id"],
use_alter=True,
name="fk_uom_american"
),
ForeignKeyConstraint(
["ace_code", "tenant_id", "company_id"],
["a76.unit_of_measure_ace.code", "a76.unit_of_measure_ace.tenant_id",
"a76.unit_of_measure_ace.company_id"],
use_alter=True,
name="fk_uom_ace"
),
ForeignKeyConstraint(
["oma_code", "tenant_id", "company_id"],
["a76.unit_of_measure_oma.code", "a76.unit_of_measure_oma.tenant_id",
"a76.unit_of_measure_oma.company_id"],
use_alter=True,
name="fk_uom_oma"
),
{"schema": "a76", "extend_existing": True}
)
id: Mapped[int] = mapped_column(
@@ -79,26 +122,44 @@ class UnitOfMeasure(Base, TenantScopedMixin, TimestampMixin):
description_en: Mapped[Optional[str]] = mapped_column(
String(100), nullable=True)
customs_code: Mapped[Optional[str]] = mapped_column(String(2), ForeignKey(
"a76.unit_of_measure_customs.code"), nullable=True) # CLAVE_AMEX
american_code: Mapped[Optional[str]] = mapped_column(String(3), ForeignKey(
"a76.unit_of_measure_american.code"), nullable=True) # CLAVE_AAMER
ace_code: Mapped[Optional[str]] = mapped_column(String(4), ForeignKey(
"a76.unit_of_measure_ace.code"), nullable=True) # CLAVEACE
oma_code: Mapped[Optional[str]] = mapped_column(String(10), ForeignKey(
"a76.unit_of_measure_oma.code"), nullable=True) # CLAVEOMA
customs_code: Mapped[Optional[str]] = mapped_column(
String(2), nullable=True) # CLAVE_AMEX
american_code: Mapped[Optional[str]] = mapped_column(
String(3), nullable=True) # CLAVE_AAMER
ace_code: Mapped[Optional[str]] = mapped_column(
String(4), nullable=True) # CLAVEACE
oma_code: Mapped[Optional[str]] = mapped_column(
String(10), nullable=True) # CLAVEOMA
# Relationships omitted for simplicity or need explicit primaryjoin
customs_unit: Mapped[Optional["UnitOfMeasureCustoms"]] = relationship()
american_unit: Mapped[Optional["UnitOfMeasureAmerican"]] = relationship()
ace_unit: Mapped[Optional["UnitOfMeasureACE"]] = relationship()
oma_unit: Mapped[Optional["UnitOfMeasureOMA"]] = relationship()
# 6. GUniMed (General/Conversion)
class UnitOfMeasureGeneral(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "units_of_measure_general"
__table_args__ = (
UniqueConstraint("code", name="uq_uom_general_code"),
{"schema": "a76"}
UniqueConstraint("code", "tenant_id", "company_id",
name="uq_uom_general_code"),
ForeignKeyConstraint(
["customs_code", "tenant_id", "company_id"],
["a76.unit_of_measure_customs.code", "a76.unit_of_measure_customs.tenant_id",
"a76.unit_of_measure_customs.company_id"],
use_alter=True,
name="fk_uom_general_customs"
),
ForeignKeyConstraint(
["ace_code", "tenant_id", "company_id"],
["a76.unit_of_measure_ace.code", "a76.unit_of_measure_ace.tenant_id",
"a76.unit_of_measure_ace.company_id"],
use_alter=True,
name="fk_uom_general_ace"
),
{"schema": "a76", "extend_existing": True}
)
id: Mapped[int] = mapped_column(
@@ -114,10 +175,10 @@ class UnitOfMeasureGeneral(Base, TenantScopedMixin, TimestampMixin):
american_unit_code: Mapped[Optional[str]
] = mapped_column(String(5), nullable=True)
customs_code: Mapped[Optional[str]] = mapped_column(String(2), ForeignKey(
"a76.unit_of_measure_customs.code"), nullable=True) # CLAVE_ADUANA
ace_code: Mapped[Optional[str]] = mapped_column(String(4), ForeignKey(
"a76.unit_of_measure_ace.code"), nullable=True) # CLAVEACE
customs_code: Mapped[Optional[str]] = mapped_column(
String(2), nullable=True) # CLAVE_ADUANA
ace_code: Mapped[Optional[str]] = mapped_column(
String(4), nullable=True) # CLAVEACE
customs_unit: Mapped[Optional["UnitOfMeasureCustoms"]] = relationship()
ace_unit: Mapped[Optional["UnitOfMeasureACE"]] = relationship()

View File

@@ -1,239 +1,97 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from core.database import get_core_db
from . import service
from .dto import (
UnitOfMeasureACECreate, UnitOfMeasureACEResponse, UnitOfMeasureACEUpdate,
UnitOfMeasureOMACreate, UnitOfMeasureOMAResponse, UnitOfMeasureOMAUpdate,
UnitOfMeasureAmericanCreate, UnitOfMeasureAmericanResponse, UnitOfMeasureAmericanUpdate,
UnitOfMeasureCustomsCreate, UnitOfMeasureCustomsResponse, UnitOfMeasureCustomsUpdate,
UnitOfMeasureCreate, UnitOfMeasureResponse, UnitOfMeasureUpdate,
UnitOfMeasureGeneralCreate, UnitOfMeasureGeneralResponse, UnitOfMeasureGeneralUpdate
)
from fastapi import APIRouter
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from . import dto, service
router = APIRouter(prefix="/units-of-measure",
tags=["a76.general_catalogs.units_of_measure"])
# --- ACE Routes ---
@router.post("/ace", response_model=UnitOfMeasureACEResponse, status_code=status.HTTP_201_CREATED)
def create_ace(data: UnitOfMeasureACECreate, session: Session = Depends(get_core_db)):
return service.create_ace(session, data)
@router.get("/ace/{id}", response_model=UnitOfMeasureACEResponse)
def get_ace(id: int, session: Session = Depends(get_core_db)):
db_obj = service.get_ace(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="ACE Unit not found")
return db_obj
@router.get("/ace", response_model=List[UnitOfMeasureACEResponse])
def get_all_ace(skip: int = 0, limit: int = 100, session: Session = Depends(get_core_db)):
return service.get_all_ace(session, skip, limit)
@router.put("/ace/{id}", response_model=UnitOfMeasureACEResponse)
def update_ace(id: int, data: UnitOfMeasureACEUpdate, session: Session = Depends(get_core_db)):
db_obj = service.get_ace(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="ACE Unit not found")
return service.update_ace(session, db_obj, data)
@router.delete("/ace/{id}", response_model=UnitOfMeasureACEResponse)
def delete_ace(id: int, session: Session = Depends(get_core_db)):
db_obj = service.get_ace(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="ACE Unit not found")
return service.delete_ace(session, db_obj)
# --- OMA Routes ---
@router.post("/oma", response_model=UnitOfMeasureOMAResponse, status_code=status.HTTP_201_CREATED)
def create_oma(data: UnitOfMeasureOMACreate, session: Session = Depends(get_core_db)):
return service.create_oma(session, data)
@router.get("/oma/{id}", response_model=UnitOfMeasureOMAResponse)
def get_oma(id: int, session: Session = Depends(get_core_db)):
db_obj = service.get_oma(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="OMA Unit not found")
return db_obj
@router.get("/oma", response_model=List[UnitOfMeasureOMAResponse])
def get_all_oma(skip: int = 0, limit: int = 100, session: Session = Depends(get_core_db)):
return service.get_all_oma(session, skip, limit)
@router.put("/oma/{id}", response_model=UnitOfMeasureOMAResponse)
def update_oma(id: int, data: UnitOfMeasureOMAUpdate, session: Session = Depends(get_core_db)):
db_obj = service.get_oma(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="OMA Unit not found")
return service.update_oma(session, db_obj, data)
@router.delete("/oma/{id}", response_model=UnitOfMeasureOMAResponse)
def delete_oma(id: int, session: Session = Depends(get_core_db)):
db_obj = service.get_oma(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="OMA Unit not found")
return service.delete_oma(session, db_obj)
# --- American Routes ---
@router.post("/american", response_model=UnitOfMeasureAmericanResponse, status_code=status.HTTP_201_CREATED)
def create_american(data: UnitOfMeasureAmericanCreate, session: Session = Depends(get_core_db)):
return service.create_american(session, data)
@router.get("/american/{id}", response_model=UnitOfMeasureAmericanResponse)
def get_american(id: int, session: Session = Depends(get_core_db)):
db_obj = service.get_american(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="American Unit not found")
return db_obj
@router.get("/american", response_model=List[UnitOfMeasureAmericanResponse])
def get_all_american(skip: int = 0, limit: int = 100, session: Session = Depends(get_core_db)):
return service.get_all_american(session, skip, limit)
@router.put("/american/{id}", response_model=UnitOfMeasureAmericanResponse)
def update_american(id: int, data: UnitOfMeasureAmericanUpdate, session: Session = Depends(get_core_db)):
db_obj = service.get_american(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="American Unit not found")
return service.update_american(session, db_obj, data)
@router.delete("/american/{id}", response_model=UnitOfMeasureAmericanResponse)
def delete_american(id: int, session: Session = Depends(get_core_db)):
db_obj = service.get_american(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="American Unit not found")
return service.delete_american(session, db_obj)
# --- Customs Routes ---
@router.post("/customs", response_model=UnitOfMeasureCustomsResponse, status_code=status.HTTP_201_CREATED)
def create_customs(data: UnitOfMeasureCustomsCreate, session: Session = Depends(get_core_db)):
return service.create_customs(session, data)
@router.get("/customs/{id}", response_model=UnitOfMeasureCustomsResponse)
def get_customs(id: int, session: Session = Depends(get_core_db)):
db_obj = service.get_customs(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="Customs Unit not found")
return db_obj
@router.get("/customs", response_model=List[UnitOfMeasureCustomsResponse])
def get_all_customs(skip: int = 0, limit: int = 100, session: Session = Depends(get_core_db)):
return service.get_all_customs(session, skip, limit)
@router.put("/customs/{id}", response_model=UnitOfMeasureCustomsResponse)
def update_customs(id: int, data: UnitOfMeasureCustomsUpdate, session: Session = Depends(get_core_db)):
db_obj = service.get_customs(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="Customs Unit not found")
return service.update_customs(session, db_obj, data)
@router.delete("/customs/{id}", response_model=UnitOfMeasureCustomsResponse)
def delete_customs(id: int, session: Session = Depends(get_core_db)):
db_obj = service.get_customs(session, id)
if not db_obj:
raise HTTPException(status_code=404, detail="Customs Unit not found")
return service.delete_customs(session, db_obj)
# --- Main UnitOfMeasure Routes ---
@router.post("/", response_model=UnitOfMeasureResponse, status_code=status.HTTP_201_CREATED)
def create_uom(data: UnitOfMeasureCreate, session: Session = Depends(get_core_db)):
return service.create_uom(session, data)
@router.get("/{id}", response_model=UnitOfMeasureResponse)
def get_uom(id: int, session: Session = Depends(get_core_db)):
db_obj = service.get_uom(session, id)
if not db_obj:
raise HTTPException(
status_code=404, detail="Unit of Measure not found")
return db_obj
@router.get("/", response_model=List[UnitOfMeasureResponse])
def get_all_uom(skip: int = 0, limit: int = 100, session: Session = Depends(get_core_db)):
return service.get_all_uom(session, skip, limit)
@router.put("/{id}", response_model=UnitOfMeasureResponse)
def update_uom(id: int, data: UnitOfMeasureUpdate, session: Session = Depends(get_core_db)):
db_obj = service.get_uom(session, id)
if not db_obj:
raise HTTPException(
status_code=404, detail="Unit of Measure not found")
return service.update_uom(session, db_obj, data)
@router.delete("/{id}", response_model=UnitOfMeasureResponse)
def delete_uom(id: int, session: Session = Depends(get_core_db)):
db_obj = service.get_uom(session, id)
if not db_obj:
raise HTTPException(
status_code=404, detail="Unit of Measure not found")
return service.delete_uom(session, db_obj)
# --- General UnitOfMeasure Routes ---
@router.post("/general", response_model=UnitOfMeasureGeneralResponse, status_code=status.HTTP_201_CREATED)
def create_uom_general(data: UnitOfMeasureGeneralCreate, session: Session = Depends(get_core_db)):
return service.create_uom_general(session, data)
@router.get("/general/{id}", response_model=UnitOfMeasureGeneralResponse)
def get_uom_general(id: int, session: Session = Depends(get_core_db)):
db_obj = service.get_uom_general(session, id)
if not db_obj:
raise HTTPException(
status_code=404, detail="General Unit of Measure not found")
return db_obj
@router.get("/general", response_model=List[UnitOfMeasureGeneralResponse])
def get_all_uom_general(skip: int = 0, limit: int = 100, session: Session = Depends(get_core_db)):
return service.get_all_uom_general(session, skip, limit)
@router.put("/general/{id}", response_model=UnitOfMeasureGeneralResponse)
def update_uom_general(id: int, data: UnitOfMeasureGeneralUpdate, session: Session = Depends(get_core_db)):
db_obj = service.get_uom_general(session, id)
if not db_obj:
raise HTTPException(
status_code=404, detail="General Unit of Measure not found")
return service.update_uom_general(session, db_obj, data)
@router.delete("/general/{id}", response_model=UnitOfMeasureGeneralResponse)
def delete_uom_general(id: int, session: Session = Depends(get_core_db)):
db_obj = service.get_uom_general(session, id)
if not db_obj:
raise HTTPException(
status_code=404, detail="General Unit of Measure not found")
return service.delete_uom_general(session, db_obj)
# ACE
ace_router = TenantCRUDRoutes(
service=service.UnitOfMeasureACEService,
create_schema=dto.UnitOfMeasureACECreate,
update_schema=dto.UnitOfMeasureACEUpdate,
response_schema=dto.UnitOfMeasureACEResponse,
prefix="/ace",
tags=["a76.general_catalogs.units_of_measure"],
resource_name="UnitOfMeasureACE",
id_name="id",
enable_list=True,
enable_filters=True,
).router
router.include_router(ace_router)
# OMA
oma_router = TenantCRUDRoutes(
service=service.UnitOfMeasureOMAService,
create_schema=dto.UnitOfMeasureOMACreate,
update_schema=dto.UnitOfMeasureOMAUpdate,
response_schema=dto.UnitOfMeasureOMAResponse,
prefix="/oma",
tags=["a76.general_catalogs.units_of_measure"],
resource_name="UnitOfMeasureOMA",
id_name="id",
enable_list=True,
enable_filters=True,
).router
router.include_router(oma_router)
# American
american_router = TenantCRUDRoutes(
service=service.UnitOfMeasureAmericanService,
create_schema=dto.UnitOfMeasureAmericanCreate,
update_schema=dto.UnitOfMeasureAmericanUpdate,
response_schema=dto.UnitOfMeasureAmericanResponse,
prefix="/american",
tags=["a76.general_catalogs.units_of_measure"],
resource_name="UnitOfMeasureAmerican",
id_name="id",
enable_list=True,
enable_filters=True,
).router
router.include_router(american_router)
# Customs
customs_router = TenantCRUDRoutes(
service=service.UnitOfMeasureCustomsService,
create_schema=dto.UnitOfMeasureCustomsCreate,
update_schema=dto.UnitOfMeasureCustomsUpdate,
response_schema=dto.UnitOfMeasureCustomsResponse,
prefix="/customs",
tags=["a76.general_catalogs.units_of_measure"],
resource_name="UnitOfMeasureCustoms",
id_name="id",
enable_list=True,
enable_filters=True,
).router
router.include_router(customs_router)
# General
general_router = TenantCRUDRoutes(
service=service.UnitOfMeasureGeneralService,
create_schema=dto.UnitOfMeasureGeneralCreate,
update_schema=dto.UnitOfMeasureGeneralUpdate,
response_schema=dto.UnitOfMeasureGeneralResponse,
prefix="/general",
tags=["a76.general_catalogs.units_of_measure"],
resource_name="UnitOfMeasureGeneral",
id_name="id",
enable_list=True,
enable_filters=True,
).router
router.include_router(general_router)
# Main UnitOfMeasure
# Note: We use prefix="" to map to /units-of-measure/
main_router = TenantCRUDRoutes(
service=service.UnitOfMeasureService,
create_schema=dto.UnitOfMeasureCreate,
update_schema=dto.UnitOfMeasureUpdate,
response_schema=dto.UnitOfMeasureResponse,
prefix="",
tags=["a76.general_catalogs.units_of_measure"],
resource_name="UnitOfMeasure",
id_name="id",
enable_list=True,
enable_filters=True,
).router
router.include_router(main_router)

View File

@@ -1,6 +1,6 @@
from typing import List, Optional, Tuple, Dict, Any, Type
from sqlalchemy import Sequence
from sqlalchemy.orm import Session
from sqlalchemy import select
from typing import Sequence, Optional, Type, TypeVar
from .models import (
UnitOfMeasureACE, UnitOfMeasureOMA, UnitOfMeasureAmerican, UnitOfMeasureCustoms,
@@ -15,169 +15,129 @@ from .dto import (
UnitOfMeasureGeneralCreate, UnitOfMeasureGeneralUpdate
)
T = TypeVar("T")
def _create(session: Session, model: Type[T], data) -> T:
db_obj = model(**data.model_dump())
session.add(db_obj)
session.commit()
session.refresh(db_obj)
return db_obj
def _get(session: Session, model: Type[T], id: int) -> Optional[T]:
return session.get(model, id)
def _get_all(session: Session, model: Type[T], skip: int = 0, limit: int = 100) -> Sequence[T]:
stmt = select(model).offset(skip).limit(limit)
result = session.execute(stmt)
return result.scalars().all()
def _update(session: Session, db_obj: T, update_data) -> T:
update_dict = update_data.model_dump(exclude_unset=True)
for key, value in update_dict.items():
setattr(db_obj, key, value)
session.commit()
session.refresh(db_obj)
return db_obj
def _delete(session: Session, db_obj: T) -> T:
session.delete(db_obj)
session.commit()
return db_obj
# --- ACE ---
def create_ace(session: Session, data: UnitOfMeasureACECreate) -> UnitOfMeasureACE:
return _create(session, UnitOfMeasureACE, data)
def get_ace(session: Session, id: int) -> Optional[UnitOfMeasureACE]:
return _get(session, UnitOfMeasureACE, id)
def get_all_ace(session: Session, skip: int = 0, limit: int = 100) -> Sequence[UnitOfMeasureACE]:
return _get_all(session, UnitOfMeasureACE, skip, limit)
def update_ace(session: Session, db_obj: UnitOfMeasureACE, data: UnitOfMeasureACEUpdate) -> UnitOfMeasureACE:
return _update(session, db_obj, data)
def delete_ace(session: Session, db_obj: UnitOfMeasureACE) -> UnitOfMeasureACE:
return _delete(session, db_obj)
# --- OMA ---
def create_oma(session: Session, data: UnitOfMeasureOMACreate) -> UnitOfMeasureOMA:
return _create(session, UnitOfMeasureOMA, data)
def get_oma(session: Session, id: int) -> Optional[UnitOfMeasureOMA]:
return _get(session, UnitOfMeasureOMA, id)
def get_all_oma(session: Session, skip: int = 0, limit: int = 100) -> Sequence[UnitOfMeasureOMA]:
return _get_all(session, UnitOfMeasureOMA, skip, limit)
def update_oma(session: Session, db_obj: UnitOfMeasureOMA, data: UnitOfMeasureOMAUpdate) -> UnitOfMeasureOMA:
return _update(session, db_obj, data)
def delete_oma(session: Session, db_obj: UnitOfMeasureOMA) -> UnitOfMeasureOMA:
return _delete(session, db_obj)
# --- American ---
def create_american(session: Session, data: UnitOfMeasureAmericanCreate) -> UnitOfMeasureAmerican:
return _create(session, UnitOfMeasureAmerican, data)
def get_american(session: Session, id: int) -> Optional[UnitOfMeasureAmerican]:
return _get(session, UnitOfMeasureAmerican, id)
def get_all_american(session: Session, skip: int = 0, limit: int = 100) -> Sequence[UnitOfMeasureAmerican]:
return _get_all(session, UnitOfMeasureAmerican, skip, limit)
def update_american(session: Session, db_obj: UnitOfMeasureAmerican, data: UnitOfMeasureAmericanUpdate) -> UnitOfMeasureAmerican:
return _update(session, db_obj, data)
def delete_american(session: Session, db_obj: UnitOfMeasureAmerican) -> UnitOfMeasureAmerican:
return _delete(session, db_obj)
# --- Customs ---
def create_customs(session: Session, data: UnitOfMeasureCustomsCreate) -> UnitOfMeasureCustoms:
return _create(session, UnitOfMeasureCustoms, data)
def get_customs(session: Session, id: int) -> Optional[UnitOfMeasureCustoms]:
return _get(session, UnitOfMeasureCustoms, id)
def get_all_customs(session: Session, skip: int = 0, limit: int = 100) -> Sequence[UnitOfMeasureCustoms]:
return _get_all(session, UnitOfMeasureCustoms, skip, limit)
def update_customs(session: Session, db_obj: UnitOfMeasureCustoms, data: UnitOfMeasureCustomsUpdate) -> UnitOfMeasureCustoms:
return _update(session, db_obj, data)
def delete_customs(session: Session, db_obj: UnitOfMeasureCustoms) -> UnitOfMeasureCustoms:
return _delete(session, db_obj)
# --- Main UnitOfMeasure ---
def create_uom(session: Session, data: UnitOfMeasureCreate) -> UnitOfMeasure:
return _create(session, UnitOfMeasure, data)
def get_uom(session: Session, id: int) -> Optional[UnitOfMeasure]:
return _get(session, UnitOfMeasure, id)
def get_all_uom(session: Session, skip: int = 0, limit: int = 100) -> Sequence[UnitOfMeasure]:
return _get_all(session, UnitOfMeasure, skip, limit)
def update_uom(session: Session, db_obj: UnitOfMeasure, data: UnitOfMeasureUpdate) -> UnitOfMeasure:
return _update(session, db_obj, data)
def delete_uom(session: Session, db_obj: UnitOfMeasure) -> UnitOfMeasure:
return _delete(session, db_obj)
# --- General UnitOfMeasure ---
def create_uom_general(session: Session, data: UnitOfMeasureGeneralCreate) -> UnitOfMeasureGeneral:
return _create(session, UnitOfMeasureGeneral, data)
def get_uom_general(session: Session, id: int) -> Optional[UnitOfMeasureGeneral]:
return _get(session, UnitOfMeasureGeneral, id)
class BaseService:
model = None
@classmethod
def get_all(
cls,
db: Session,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 100,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[Any], int]:
query = db.query(cls.model).filter(
cls.model.tenant_id == tenant_id,
cls.model.company_id == company_id,
)
if filters:
if filters.get("code"):
query = query.filter(
cls.model.code.ilike(f"%{filters['code']}%"))
if filters.get("description"):
if hasattr(cls.model, "description"):
query = query.filter(cls.model.description.ilike(
f"%{filters['description']}%"))
total = query.count()
items = query.offset(skip).limit(limit).all()
return items, total
@classmethod
def get_by_id(
cls, db: Session, id: int, tenant_id: int, company_id: int
) -> Optional[Any]:
return db.query(cls.model).filter(
cls.model.id == id,
cls.model.tenant_id == tenant_id,
cls.model.company_id == company_id,
).first()
@classmethod
def create(
cls,
db: Session,
data: Any,
tenant_id: int,
company_id: int,
) -> Any:
db_obj = cls.model(
**data.model_dump(), tenant_id=tenant_id, company_id=company_id
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
@classmethod
def update(
cls,
db: Session,
id: int,
tenant_id: int,
data: Any,
company_id: int,
) -> Optional[Any]:
db_obj = cls.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():
setattr(db_obj, key, value)
db.commit()
db.refresh(db_obj)
return db_obj
@classmethod
def delete(
cls, db: Session, id: int, tenant_id: int, company_id: int
) -> bool:
db_obj = cls.get_by_id(db, id, tenant_id, company_id)
if not db_obj:
return False
db.delete(db_obj)
db.commit()
return True
class UnitOfMeasureACEService(BaseService):
model = UnitOfMeasureACE
class UnitOfMeasureOMAService(BaseService):
model = UnitOfMeasureOMA
class UnitOfMeasureAmericanService(BaseService):
model = UnitOfMeasureAmerican
class UnitOfMeasureCustomsService(BaseService):
model = UnitOfMeasureCustoms
class UnitOfMeasureService(BaseService):
model = UnitOfMeasure
class UnitOfMeasureGeneralService(BaseService):
model = UnitOfMeasureGeneral
def get_all_uom_general(session: Session, skip: int = 0, limit: int = 100) -> Sequence[UnitOfMeasureGeneral]:
return _get_all(session, UnitOfMeasureGeneral, skip, limit)
return BaseService._get_all(session, UnitOfMeasureGeneral, skip, limit)
def update_uom_general(session: Session, db_obj: UnitOfMeasureGeneral, data: UnitOfMeasureGeneralUpdate) -> UnitOfMeasureGeneral:
return _update(session, db_obj, data)
return BaseService._update(session, db_obj, data)
def delete_uom_general(session: Session, db_obj: UnitOfMeasureGeneral) -> UnitOfMeasureGeneral:
return _delete(session, db_obj)
return BaseService._delete(session, db_obj)

View File

@@ -42,6 +42,11 @@ class Part(Base, TenantScopedMixin, TimestampMixin):
ForeignKeyConstraint(
["currency_key"], ["public.currency_types.code"], name="fk_parts_currency"
),
ForeignKeyConstraint(
["unit_of_measure", "tenant_id", "company_id"],
["a76.units_of_measure.code", "a76.units_of_measure.tenant_id",
"a76.units_of_measure.company_id"],
),
UniqueConstraint(
"tenant_id", "company_id", "part_number", name="client_part_ukey"
),
@@ -60,7 +65,7 @@ class Part(Base, TenantScopedMixin, TimestampMixin):
description_english: Mapped[Optional[str]] = mapped_column(String(500))
part_class: Mapped[Optional[str]] = mapped_column(String(8))
unit_of_measure: Mapped[Optional[str]] = mapped_column(
String(5), ForeignKey("a76.units_of_measure.code")
String(5)
)
commercial_part_number: Mapped[Optional[str]] = mapped_column(String(70))
country_of_origin: Mapped[Optional[str]] = mapped_column(String(3))

View File

@@ -8,24 +8,40 @@ from fastapi import APIRouter
from .customs_brokers.routes import router as customs_broker_router
# Importar routers de módulos
from .auth import router as auth_router
from ..core.auth import router as auth_router
from .classes import router as classes_router
from .clients_and_providers import router as client_and_provider_router
from .general_catalogs.company import router as company_router
from .country_rule_oct.routes import router as country_rule_oct_router
from .transportation.drivers.routes import router as drivers_router
from .general_catalogs.exchange_rate.routes import router as exchange_rate_router
from .general_catalogs.identifiers.routes import router as identifiers_router
from .fraction_rule_octave.routes import router as fraction_rule_octave_router
from .licenses import router as licenses_router
from ..core.licenses import router as licenses_router
from .general_catalogs.packages.routes import router as package_router
from .general_catalogs.ports.routes import router as ports_router
from .parts import router as parts_router
from .pedmientos.router import router as pedimentos_router
from .permission_rule_oct.routes import router as permission_rule_oct_router
from .general_catalogs.seal.routes import router as seal_router
from .tenants import router as tenants_router
from .general_catalogs.units_of_measure.routes import router as units_of_measure_router
from .general_catalogs.concepts.routes import router as concepts_router
from .general_catalogs.customs_broker_concepts.routes import router as customs_broker_concepts_router
from .general_catalogs.classification_concepts.routes import router as classification_concepts_router
from .general_catalogs.unit_conversions.routes import router as unit_conversions_router
from .general_catalogs.equivalencies.routes import router as equivalencies_router
from .general_catalogs.multi_currency_types.routes import router as multi_currency_types_router
from .general_catalogs.inpc.routes import router as inpc_router
from .general_catalogs.legends.routes import router as legends_router
from .general_catalogs.signatures.routes import router as signatures_router
from .general_catalogs.error_catalogs.routes import router as error_catalogs_router
from .general_catalogs.doda.routes import router as doda_router
from .general_catalogs.prevalidators.routes import router as prevalidators_router
from .general_catalogs.electronic_notices.routes import router as electronic_notices_router
from ..core.tenants import router as tenants_router
from .transportation.trailers.routes import router as trailers_router
from .transportation.transporters.routes import router as transporters_router
from .user_tenant.routes import router as user_tenant_router
from ..core.user_tenant.routes import router as user_tenant_router
from .transportation.vehicles.routes import router as vehicles_router
# Router principal
@@ -34,7 +50,8 @@ router = APIRouter()
# Registrar módulos
router.include_router(auth_router)
router.include_router(tenants_router, prefix="/a76", tags=["a76 / tenants"])
router.include_router(user_tenant_router, prefix="/a76", tags=["a76 / user-tenants"])
router.include_router(user_tenant_router, prefix="/a76",
tags=["a76 / user-tenants"])
router.include_router(licenses_router, prefix="/a76", tags=["a76 / licenses"])
router.include_router(pedimentos_router, prefix="/a76")
router.include_router(
@@ -47,18 +64,38 @@ router.include_router(
permission_rule_oct_router, prefix="/a76", tags=["a76 / permission_rule_oct"]
)
router.include_router(package_router, prefix="/a76")
router.include_router(ports_router, prefix="/a76")
router.include_router(seal_router, prefix="/a76", tags=["a76 / seal"])
router.include_router(units_of_measure_router, prefix="/a76")
router.include_router(
fraction_rule_octave_router, prefix="/a76", tags=["a76 / fraction_rule_octave"]
)
router.include_router(identifiers_router, prefix="/a76")
router.include_router(
country_rule_oct_router, prefix="/a76", tags=["a76 / country_rule_oct"]
)
router.include_router(exchange_rate_router, prefix="/a76", tags=["a76 / exchange_rate"])
router.include_router(exchange_rate_router, prefix="/a76",
tags=["a76 / exchange_rate"])
router.include_router(trailers_router, prefix="/a76", tags=["a76 / trailers"])
router.include_router(
customs_broker_router, prefix="/a76", tags=["a76 / customs_broker"]
)
router.include_router(drivers_router, prefix="/a76", tags=["a76 / drivers"])
router.include_router(transporters_router, prefix="/a76", tags=["a76 / transporters"])
router.include_router(transporters_router, prefix="/a76",
tags=["a76 / transporters"])
router.include_router(vehicles_router, prefix="/a76", tags=["a76 / vehicles"])
# Registrar catálogos generales adicionales
router.include_router(concepts_router, prefix="/a76")
router.include_router(customs_broker_concepts_router, prefix="/a76")
router.include_router(classification_concepts_router, prefix="/a76")
router.include_router(unit_conversions_router, prefix="/a76")
router.include_router(equivalencies_router, prefix="/a76")
router.include_router(multi_currency_types_router, prefix="/a76")
router.include_router(inpc_router, prefix="/a76")
router.include_router(legends_router, prefix="/a76")
router.include_router(signatures_router, prefix="/a76")
router.include_router(error_catalogs_router, prefix="/a76")
router.include_router(doda_router, prefix="/a76")
router.include_router(prevalidators_router, prefix="/a76")
router.include_router(electronic_notices_router, prefix="/a76")

View File

@@ -4,8 +4,8 @@ Servicio de autenticación con Keycloak
import logging
from api.v1.modules.a76.tenants.service import TenantService
from api.v1.modules.a76.user_tenant.service import UserTenantService
from api.v1.modules.core.tenants.service import TenantService
from api.v1.modules.core.user_tenant.service import UserTenantService
from core.config import settings
from fastapi import HTTPException
from keycloak import KeycloakAdmin, KeycloakOpenID
@@ -266,7 +266,7 @@ class AuthService:
"""
try:
# Verificar que el tenant existe
from api.v1.modules.a76.tenants.service import TenantService
from api.v1.modules.core.tenants.service import TenantService
tenant_service = TenantService(self.db)
tenant = tenant_service.get_tenant_by_slug(register_data.tenant_slug)
@@ -319,7 +319,7 @@ class AuthService:
# Agregar el usuario al tenant en la base de datos
try:
from api.v1.modules.a76.user_tenant.service import UserTenantService
from api.v1.modules.core.user_tenant.service import UserTenantService
user_tenant_service = UserTenantService(self.db)
user_tenant_service.add_user_to_tenant(

View File

@@ -36,11 +36,11 @@ class License(Base, TimestampMixin):
"""
__tablename__ = "licenses"
__table_args__ = {"schema": "a76"}
__table_args__ = {"schema": "core"}
id = Column(Integer, primary_key=True, index=True)
tenant_id = Column(
Integer, ForeignKey("a76.tenants.id"), nullable=False, unique=True, index=True
Integer, ForeignKey("core.tenants.id"), nullable=False, unique=True, index=True
)
# Plan y características
@@ -74,11 +74,11 @@ class LicenseUsage(Base, TimestampMixin):
"""
__tablename__ = "license_usage"
__table_args__ = {"schema": "a76"}
__table_args__ = {"schema": "core"}
id = Column(Integer, primary_key=True, index=True)
tenant_id = Column(
Integer, ForeignKey("a76.tenants.id"), nullable=False, index=True
Integer, ForeignKey("core.tenants.id"), nullable=False, index=True
)
# Métricas de uso

View File

@@ -13,7 +13,7 @@ from sqlalchemy import Integer, String, Text
from sqlalchemy.orm import Mapped, relationship
if TYPE_CHECKING:
from api.v1.modules.a76.user_tenant.models import UserTenant
from api.v1.modules.core.user_tenant.models import UserTenant
class TenantType(enum.Enum):
@@ -30,7 +30,7 @@ class Tenant(Base, TimestampMixin):
"""
__tablename__ = "tenants"
__table_args__ = {"schema": "a76", "extend_existing": True}
__table_args__ = {"schema": "core", "extend_existing": True}
id = Column(Integer, primary_key=True, index=True)
name = Column(String(255), nullable=False, index=True)

View File

@@ -10,7 +10,7 @@ from sqlalchemy import Boolean, ForeignKeyConstraint, String, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
if TYPE_CHECKING:
from api.v1.modules.a76.tenants.models import Tenant
from api.v1.modules.core.tenants.models import Tenant
class UserTenant(Base, TenantScopedMixin, TimestampMixin):
@@ -26,7 +26,7 @@ class UserTenant(Base, TenantScopedMixin, TimestampMixin):
UniqueConstraint(
"keycloak_user_id", "tenant_id", "company_id", name="uq_user_tenant"
),
{"schema": "a76", "extend_existing": True},
{"schema": "core", "extend_existing": True},
)
# Primary Key

View File

@@ -19,12 +19,13 @@ class CodePedimentoRegimen(Base):
["regimen_code"], ["public.pedimento_regimens.code"], name="fk_regimenped"
),
PrimaryKeyConstraint("id", name="clave_pedimento_regimens_pkey"),
{"schema": "public"},
{"schema": "public", "extend_existing": True},
)
id: Mapped[int] = mapped_column(Integer)
pedimento_code: Mapped[str] = mapped_column(String(3), nullable=False)
regimen_code: Mapped[Optional[str]] = mapped_column(String(3), nullable=False)
regimen_code: Mapped[Optional[str]] = mapped_column(
String(3), nullable=False)
type_code: Mapped[Optional[str]] = mapped_column(
String(1)
) # si aplica un tipo de relación

View File

@@ -7,7 +7,7 @@ class Container(Base):
__tablename__ = "containers" # GContenedores
__table_args__ = (
PrimaryKeyConstraint("key", name="containers_pkey"),
{"schema": "public"}, # opcional
{"schema": "public", "extend_existing": True}, # opcional
)
key: Mapped[str] = mapped_column(

View File

@@ -8,11 +8,13 @@ class Country(Base):
__table_args__ = (
PrimaryKeyConstraint("m3_key", name="countries_pkey"),
Index("ak_country_ame", "ame_key", unique=True),
{"schema": "public"}, # opcional
{"schema": "public", "extend_existing": True}, # opcional
)
m3_key: Mapped[str] = mapped_column(String(3), nullable=False) # clave M3
mex_key: Mapped[str] = mapped_column(String(2), nullable=False) # clave país México
m3_key: Mapped[str] = mapped_column(
String(3), primary_key=True, nullable=False) # clave M3
mex_key: Mapped[str] = mapped_column(
String(2), nullable=False) # clave país México
ame_key: Mapped[str] = mapped_column(
String(2), nullable=False
) # clave país América / regional

View File

@@ -7,11 +7,11 @@ class CurrencyType(Base):
__tablename__ = "currency_types" # GTiposMoneda
__table_args__ = (
PrimaryKeyConstraint("code", name="currency_types_pkey"),
{"schema": "public"}, # opcional
{"schema": "public", "extend_existing": True},
)
code: Mapped[str] = mapped_column(
String(3), nullable=False
String(3), primary_key=True, nullable=False
) # código ISO o clave de moneda
currency_name: Mapped[str] = mapped_column(
String(15), nullable=False

View File

@@ -7,7 +7,7 @@ class CustomsSection(Base):
__tablename__ = "customs_sections" # GAduanaSec
__table_args__ = (
PrimaryKeyConstraint("customs_code", name="customs_code_pkey"),
{"schema": "public"},
{"schema": "public", "extend_existing": True},
)
customs_code = mapped_column(String(3), nullable=False)

View File

@@ -7,11 +7,13 @@ class CustomsWarehouse(Base):
__tablename__ = "customs_warehouses" # GRecintos
__table_args__ = (
PrimaryKeyConstraint("key", "customs", name="pk_customs_warehouse"),
{"schema": "public"}, # opcional
{"schema": "public", "extend_existing": True}, # opcional
)
key: Mapped[str] = mapped_column(String(3), nullable=False) # clave del recinto
customs: Mapped[str] = mapped_column(String(100), nullable=False) # aduana asociada
key: Mapped[str] = mapped_column(
String(3), nullable=False) # clave del recinto
customs: Mapped[str] = mapped_column(
String(100), nullable=False) # aduana asociada
fiscalized_warehouse: Mapped[str] = mapped_column(
String(1000)
) # recintos fiscalizados (valor legal)

View File

@@ -7,7 +7,7 @@ class Incoterm(Base):
__tablename__ = "incoterms" # GIncoterm
__table_args__ = (
PrimaryKeyConstraint("code", name="incoterms_pkey"),
{"schema": "public"},
{"schema": "public", "extend_existing": True},
)
code: Mapped[str] = mapped_column(String(5), nullable=False)

View File

@@ -7,7 +7,7 @@ class InvoiceType(Base):
__tablename__ = "invoice_types" # GTiposFactura
__table_args__ = (
PrimaryKeyConstraint("key", name="invoice_types_pkey"),
{"schema": "public"}, # opcional
{"schema": "public", "extend_existing": True}, # opcional
)
key: Mapped[str] = mapped_column(
@@ -16,7 +16,8 @@ class InvoiceType(Base):
description: Mapped[str] = mapped_column(
String(50), nullable=False
) # descripción oficial (en español)
note: Mapped[str] = mapped_column(String(500)) # observación o comentario adicional
# observación o comentario adicional
note: Mapped[str] = mapped_column(String(500))
type: Mapped[str] = mapped_column(String(15)) # tipo
def __repr__(self):

View File

@@ -7,10 +7,11 @@ class MaterialType(Base):
__tablename__ = "material_types" # STipoMat QTipoActFijo
__table_args__ = (
PrimaryKeyConstraint("key", name="material_types_pkey"),
{"schema": "public"},
{"schema": "public", "extend_existing": True},
)
key: Mapped[str] = mapped_column(String(10), nullable=False) # clave del material
key: Mapped[str] = mapped_column(
String(10), nullable=False) # clave del material
type: Mapped[str] = mapped_column(String(15), nullable=False) # tipo
description: Mapped[str] = mapped_column(
String(256), nullable=False

View File

@@ -7,7 +7,7 @@ class PaymentMethod(Base):
__tablename__ = "payment_methods" # GFormaPago
__table_args__ = (
PrimaryKeyConstraint("key", name="payment_methods_pkey"),
{"schema": "public"}, # opcional
{"schema": "public", "extend_existing": True}, # opcional
)
key: Mapped[str] = mapped_column(String(2), nullable=False)

View File

@@ -12,7 +12,7 @@ class PedimentoCode(Base):
__tablename__ = "pedimento_codes" # GClavePed
__table_args__ = (
PrimaryKeyConstraint("code", name="pedimento_codes_pkey"),
{"schema": "public"}, # esquema del anexo 22
{"schema": "public", "extend_existing": True}, # esquema del anexo 22
)
code: Mapped[str] = mapped_column(String(3), nullable=False)

View File

@@ -12,7 +12,7 @@ class RegimenPedimento(Base):
__tablename__ = "pedimento_regimens" # GRegimenPed
__table_args__ = (
PrimaryKeyConstraint("code", name="pedimento_regimens_pkey"),
{"schema": "public"},
{"schema": "public", "extend_existing": True},
)
code: Mapped[str] = mapped_column(

View File

@@ -7,10 +7,11 @@ class Sector(Base):
__tablename__ = "sectors" # GSectores
__table_args__ = (
PrimaryKeyConstraint("key", name="sectors_pkey"),
{"schema": "public"}, # opcional
{"schema": "public", "extend_existing": True}, # opcional
)
key: Mapped[str] = mapped_column(String(8), nullable=False) # clave del sector
key: Mapped[str] = mapped_column(
String(8), nullable=False) # clave del sector
description: Mapped[str] = mapped_column(
String(150), nullable=False
) # descripción oficial (en español)

View File

@@ -9,7 +9,7 @@ class State(Base):
__tablename__ = "states" # GEstados
__table_args__ = (
PrimaryKeyConstraint("m3_key", "description", name="states_pkey"),
{"schema": "public"},
{"schema": "public", "extend_existing": True},
)
m3_key: Mapped[str] = mapped_column(String(3), nullable=False)

View File

@@ -7,7 +7,7 @@ class TransportMode(Base):
__tablename__ = "transport_modes" # GModTransporte
__table_args__ = (
PrimaryKeyConstraint("key", name="transport_modes_pkey"),
{"schema": "public"}, # opcional
{"schema": "public", "extend_existing": True}, # opcional
)
key: Mapped[str] = mapped_column(String(3), nullable=False)

View File

@@ -7,7 +7,7 @@ class TransportType(Base):
__tablename__ = "transport_types" # GTiposTransporte
__table_args__ = (
PrimaryKeyConstraint("transport_code", name="transport_types_pkey"),
{"schema": "public"},
{"schema": "public", "extend_existing": True},
)
transport_code: Mapped[str] = mapped_column(

View File

@@ -7,7 +7,7 @@ class ValuationMethod(Base):
__tablename__ = "valuation_methods" # GMetValor
__table_args__ = (
PrimaryKeyConstraint("key", name="valuation_methods_pkey"),
{"schema": "public"},
{"schema": "public", "extend_existing": True},
)
key: Mapped[str] = mapped_column(String(2), nullable=False)

View File

@@ -4,15 +4,19 @@ Configuración de base de datos con soporte multi-tenant
- Bases de datos dedicadas para clientes enterprise
"""
import logging
from contextlib import contextmanager
from typing import AsyncGenerator, Dict, Generator, Optional
from sqlalchemy import create_engine
from sqlalchemy.exc import ProgrammingError
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import Session, declarative_base, sessionmaker
from .config import settings
logger = logging.getLogger(__name__)
# Base declarativa para modelos ORM
Base = declarative_base()
@@ -25,7 +29,8 @@ core_engine = create_engine(
echo=False,
)
CoreSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=core_engine)
CoreSessionLocal = sessionmaker(
autocommit=False, autoflush=False, bind=core_engine)
# Engine asíncrono para operaciones async
async_core_engine = create_async_engine(
@@ -106,7 +111,8 @@ def get_tenant_db(
else:
# Tenant con BD dedicada
engine = get_tenant_engine(tenant_id, db_config)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
SessionLocal = sessionmaker(
autocommit=False, autoflush=False, bind=engine)
db = SessionLocal()
try:
@@ -119,7 +125,15 @@ def init_db():
"""
Inicializa las tablas de la base de datos core
"""
Base.metadata.create_all(bind=core_engine)
try:
Base.metadata.create_all(bind=core_engine, checkfirst=True)
except ProgrammingError as e:
# Si la tabla ya existe, es seguro continuar
if "already exists" in str(e):
logger.warning(
f"Algunas tablas ya existen en la base de datos: {e}")
else:
raise
async def init_async_db():

View File

@@ -0,0 +1,61 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface ClassificationConcept {
id: number;
classification: string;
description?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface ClassificationConceptCreate {
classification: string;
description?: string;
}
export interface ClassificationConceptUpdate extends Partial<ClassificationConceptCreate> {}
export interface ClassificationConceptListResponse {
items: ClassificationConcept[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getClassificationConcepts(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<ClassificationConceptListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/classification_concepts?${params.toString()}`);
return response.data;
}
export async function getClassificationConcept(id: number): Promise<ClassificationConcept> {
const response = await api.get(`/a76/classification_concepts/${id}`);
return response.data;
}
export async function createClassificationConcept(data: ClassificationConceptCreate): Promise<ClassificationConcept> {
const response = await api.post('/a76/classification_concepts', data);
return response.data;
}
export async function updateClassificationConcept(id: number, data: ClassificationConceptUpdate): Promise<ClassificationConcept> {
const response = await api.patch(`/a76/classification_concepts/${id}`, data);
return response.data;
}
export async function deleteClassificationConcept(id: number): Promise<void> {
await api.delete(`/a76/classification_concepts/${id}`);
}

View File

@@ -0,0 +1,91 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface Company {
id: number;
tenant_id: number;
name: string | null;
rfc: string | null;
main_activity: string | null;
program: string | null;
program_number: string | null;
prosec: number | null;
prosec_authorization: string | null;
manufacturer_id: string | null;
broker_company: string | null;
responsible: string | null;
responsible_name: string | null;
responsible_last_name: string | null;
responsible_mother_last_name: string | null;
created_at: string | null;
updated_at: string | null;
}
export interface CompanyCreate {
name?: string | null;
rfc?: string | null;
main_activity?: string | null;
program?: string | null;
program_number?: string | null;
prosec?: number | null;
prosec_authorization?: string | null;
manufacturer_id?: string | null;
broker_company?: string | null;
responsible?: string | null;
responsible_name?: string | null;
responsible_last_name?: string | null;
responsible_mother_last_name?: string | null;
}
export interface CompanyUpdate {
name?: string | null;
rfc?: string | null;
main_activity?: string | null;
program?: string | null;
program_number?: string | null;
prosec?: number | null;
prosec_authorization?: string | null;
manufacturer_id?: string | null;
broker_company?: string | null;
responsible?: string | null;
responsible_name?: string | null;
responsible_last_name?: string | null;
responsible_mother_last_name?: string | null;
}
export interface CompanyListResponse {
items: Company[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getCompanies(
page = 1,
pageSize = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<CompanyListResponse>> {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
return await api.get(`/a76/company?${queryParams.toString()}`);
}
export async function getCompany(id: number): Promise<ApiResponse<Company>> {
return await api.get(`/a76/company/${id}`);
}
export async function createCompany(data: CompanyCreate): Promise<ApiResponse<Company>> {
return await api.post(`/a76/company`, data);
}
export async function updateCompany(id: number, data: CompanyUpdate): Promise<ApiResponse<Company>> {
return await api.put(`/a76/company/${id}`, data);
}
export async function deleteCompany(id: number): Promise<ApiResponse<void>> {
return await api.delete(`/a76/company/${id}`);
}

View File

@@ -0,0 +1,79 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface Concept {
id: number;
code: string;
description?: string;
description_en?: string;
detailed_description?: string;
priority?: number;
priority_ame?: number;
first_total?: boolean;
type?: string;
is_printed?: boolean;
section?: number;
classification?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface ConceptCreate {
code: string;
description?: string;
description_en?: string;
detailed_description?: string;
priority?: number;
priority_ame?: number;
first_total?: boolean;
type?: string;
is_printed?: boolean;
section?: number;
classification?: string;
}
export interface ConceptUpdate extends Partial<ConceptCreate> {}
export interface ConceptListResponse {
items: Concept[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getConcepts(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<ConceptListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/concepts?${params.toString()}`);
return response.data;
}
export async function getConcept(id: number): Promise<Concept> {
const response = await api.get(`/a76/concepts/${id}`);
return response.data;
}
export async function createConcept(data: ConceptCreate): Promise<Concept> {
const response = await api.post('/a76/concepts', data);
return response.data;
}
export async function updateConcept(id: number, data: ConceptUpdate): Promise<Concept> {
const response = await api.patch(`/a76/concepts/${id}`, data);
return response.data;
}
export async function deleteConcept(id: number): Promise<void> {
await api.delete(`/a76/concepts/${id}`);
}

View File

@@ -0,0 +1,77 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface CustomsBrokerConcept {
id: number;
code: string;
description?: string;
description_en?: string;
detailed_description?: string;
priority?: number;
first_total?: boolean;
type?: string;
is_printed?: boolean;
section?: number;
classification?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface CustomsBrokerConceptCreate {
code: string;
description?: string;
description_en?: string;
detailed_description?: string;
priority?: number;
first_total?: boolean;
type?: string;
is_printed?: boolean;
section?: number;
classification?: string;
}
export interface CustomsBrokerConceptUpdate extends Partial<CustomsBrokerConceptCreate> {}
export interface CustomsBrokerConceptListResponse {
items: CustomsBrokerConcept[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getCustomsBrokerConcepts(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<CustomsBrokerConceptListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/customs_broker_concepts?${params.toString()}`);
return response.data;
}
export async function getCustomsBrokerConcept(id: number): Promise<CustomsBrokerConcept> {
const response = await api.get(`/a76/customs_broker_concepts/${id}`);
return response.data;
}
export async function createCustomsBrokerConcept(data: CustomsBrokerConceptCreate): Promise<CustomsBrokerConcept> {
const response = await api.post('/a76/customs_broker_concepts', data);
return response.data;
}
export async function updateCustomsBrokerConcept(id: number, data: CustomsBrokerConceptUpdate): Promise<CustomsBrokerConcept> {
const response = await api.patch(`/a76/customs_broker_concepts/${id}`, data);
return response.data;
}
export async function deleteCustomsBrokerConcept(id: number): Promise<void> {
await api.delete(`/a76/customs_broker_concepts/${id}`);
}

View File

@@ -0,0 +1,61 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface DODA {
id: number;
code: string;
description?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface DODACreate {
code: string;
description?: string;
}
export interface DODAUpdate extends Partial<DODACreate> {}
export interface DODAListResponse {
items: DODA[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getDODAs(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<DODAListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/doda?${params.toString()}`);
return response.data;
}
export async function getDODA(id: number): Promise<DODA> {
const response = await api.get(`/a76/doda/${id}`);
return response.data;
}
export async function createDODA(data: DODACreate): Promise<DODA> {
const response = await api.post('/a76/doda', data);
return response.data;
}
export async function updateDODA(id: number, data: DODAUpdate): Promise<DODA> {
const response = await api.patch(`/a76/doda/${id}`, data);
return response.data;
}
export async function deleteDODA(id: number): Promise<void> {
await api.delete(`/a76/doda/${id}`);
}

View File

@@ -0,0 +1,61 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface ElectronicNotice {
id: number;
code: string;
description?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface ElectronicNoticeCreate {
code: string;
description?: string;
}
export interface ElectronicNoticeUpdate extends Partial<ElectronicNoticeCreate> {}
export interface ElectronicNoticeListResponse {
items: ElectronicNotice[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getElectronicNotices(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<ElectronicNoticeListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/electronic_notices?${params.toString()}`);
return response.data;
}
export async function getElectronicNotice(id: number): Promise<ElectronicNotice> {
const response = await api.get(`/a76/electronic_notices/${id}`);
return response.data;
}
export async function createElectronicNotice(data: ElectronicNoticeCreate): Promise<ElectronicNotice> {
const response = await api.post('/a76/electronic_notices', data);
return response.data;
}
export async function updateElectronicNotice(id: number, data: ElectronicNoticeUpdate): Promise<ElectronicNotice> {
const response = await api.patch(`/a76/electronic_notices/${id}`, data);
return response.data;
}
export async function deleteElectronicNotice(id: number): Promise<void> {
await api.delete(`/a76/electronic_notices/${id}`);
}

View File

@@ -0,0 +1,63 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface Equivalency {
id: number;
fraccion_mex: string;
fraccion_us: string;
description?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface EquivalencyCreate {
fraccion_mex: string;
fraccion_us: string;
description?: string;
}
export interface EquivalencyUpdate extends Partial<EquivalencyCreate> {}
export interface EquivalencyListResponse {
items: Equivalency[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getEquivalencies(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<EquivalencyListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/equivalencies?${params.toString()}`);
return response.data;
}
export async function getEquivalency(id: number): Promise<Equivalency> {
const response = await api.get(`/a76/equivalencies/${id}`);
return response.data;
}
export async function createEquivalency(data: EquivalencyCreate): Promise<Equivalency> {
const response = await api.post('/a76/equivalencies', data);
return response.data;
}
export async function updateEquivalency(id: number, data: EquivalencyUpdate): Promise<Equivalency> {
const response = await api.patch(`/a76/equivalencies/${id}`, data);
return response.data;
}
export async function deleteEquivalency(id: number): Promise<void> {
await api.delete(`/a76/equivalencies/${id}`);
}

View File

@@ -0,0 +1,61 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface ErrorCatalog {
id: number;
code: string;
description?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface ErrorCatalogCreate {
code: string;
description?: string;
}
export interface ErrorCatalogUpdate extends Partial<ErrorCatalogCreate> {}
export interface ErrorCatalogListResponse {
items: ErrorCatalog[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getErrorCatalogs(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<ErrorCatalogListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/error_catalogs?${params.toString()}`);
return response.data;
}
export async function getErrorCatalog(id: number): Promise<ErrorCatalog> {
const response = await api.get(`/a76/error_catalogs/${id}`);
return response.data;
}
export async function createErrorCatalog(data: ErrorCatalogCreate): Promise<ErrorCatalog> {
const response = await api.post('/a76/error_catalogs', data);
return response.data;
}
export async function updateErrorCatalog(id: number, data: ErrorCatalogUpdate): Promise<ErrorCatalog> {
const response = await api.patch(`/a76/error_catalogs/${id}`, data);
return response.data;
}
export async function deleteErrorCatalog(id: number): Promise<void> {
await api.delete(`/a76/error_catalogs/${id}`);
}

View File

@@ -0,0 +1,62 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface Identifier {
id: number;
code: string;
description: string | null;
level: string | null;
complement: string | null;
company_id: number;
tenant_id: number;
created_at: string | null;
updated_at: string | null;
}
export interface IdentifierCreate {
code: string;
description?: string | null;
level?: string | null;
complement?: string | null;
company_id: number;
}
export interface IdentifierUpdate {
code?: string;
description?: string | null;
level?: string | null;
complement?: string | null;
}
export interface IdentifierListResponse {
items: Identifier[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getIdentifiers(
page = 1,
pageSize = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<IdentifierListResponse>> {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
return await api.get(`/a76/identifiers?${queryParams.toString()}`);
}
export async function createIdentifier(data: IdentifierCreate): Promise<ApiResponse<Identifier>> {
return await api.post('/a76/identifiers', data);
}
export async function updateIdentifier(id: number, data: IdentifierUpdate): Promise<ApiResponse<Identifier>> {
return await api.put(`/a76/identifiers/${id}`, data);
}
export async function deleteIdentifier(id: number): Promise<ApiResponse<void>> {
return await api.delete(`/a76/identifiers/${id}`);
}

View File

@@ -0,0 +1,21 @@
/**
* Índice de exportación para catálogos generales A76
*/
// Unit Measures - Main
export * from './unit-measures';
// Unit Measures - Customs (Mexican)
export * from './um-customs-mex';
// Unit Measures - American
export * from './um-customs-ame';
// Unit Measures - ACE
export * from './um-ace';
// Unit Measures - OMA
export * from './um-oma';
// Locations (from ports)
export * from './locations';

View File

@@ -0,0 +1,63 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface INPC {
id: number;
year: string;
month: string;
value?: number;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface INPCCreate {
year: string;
month: string;
value?: number;
}
export interface INPCUpdate extends Partial<INPCCreate> {}
export interface INPCListResponse {
items: INPC[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getINPCs(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<INPCListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/inpc?${params.toString()}`);
return response.data;
}
export async function getINPC(id: number): Promise<INPC> {
const response = await api.get(`/a76/inpc/${id}`);
return response.data;
}
export async function createINPC(data: INPCCreate): Promise<INPC> {
const response = await api.post('/a76/inpc', data);
return response.data;
}
export async function updateINPC(id: number, data: INPCUpdate): Promise<INPC> {
const response = await api.patch(`/a76/inpc/${id}`, data);
return response.data;
}
export async function deleteINPC(id: number): Promise<void> {
await api.delete(`/a76/inpc/${id}`);
}

View File

@@ -0,0 +1,61 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface Legend {
id: number;
code: string;
description?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface LegendCreate {
code: string;
description?: string;
}
export interface LegendUpdate extends Partial<LegendCreate> {}
export interface LegendListResponse {
items: Legend[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getLegends(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<LegendListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/legends?${params.toString()}`);
return response.data;
}
export async function getLegend(id: number): Promise<Legend> {
const response = await api.get(`/a76/legends/${id}`);
return response.data;
}
export async function createLegend(data: LegendCreate): Promise<Legend> {
const response = await api.post('/a76/legends', data);
return response.data;
}
export async function updateLegend(id: number, data: LegendUpdate): Promise<Legend> {
const response = await api.patch(`/a76/legends/${id}`, data);
return response.data;
}
export async function deleteLegend(id: number): Promise<void> {
await api.delete(`/a76/legends/${id}`);
}

Some files were not shown because too many files have changed in this diff Show More