diff --git a/backend/api/v1/modules/a76/classes/models.py b/backend/api/v1/modules/a76/classes/models.py index 240238e9..e12374f7 100644 --- a/backend/api/v1/modules/a76/classes/models.py +++ b/backend/api/v1/modules/a76/classes/models.py @@ -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 diff --git a/backend/api/v1/modules/a76/general_catalogs/classification_concepts/models.py b/backend/api/v1/modules/a76/general_catalogs/classification_concepts/models.py index c13ce781..9789731e 100644 --- a/backend/api/v1/modules/a76/general_catalogs/classification_concepts/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/classification_concepts/models.py @@ -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( diff --git a/backend/api/v1/modules/a76/general_catalogs/classification_concepts/routes.py b/backend/api/v1/modules/a76/general_catalogs/classification_concepts/routes.py index 13b01ccd..9f9ba069 100644 --- a/backend/api/v1/modules/a76/general_catalogs/classification_concepts/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/classification_concepts/routes.py @@ -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 diff --git a/backend/api/v1/modules/a76/general_catalogs/classification_concepts/service.py b/backend/api/v1/modules/a76/general_catalogs/classification_concepts/service.py index ee4509e7..33245a6d 100644 --- a/backend/api/v1/modules/a76/general_catalogs/classification_concepts/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/classification_concepts/service.py @@ -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 diff --git a/backend/api/v1/modules/a76/general_catalogs/company/routes.py b/backend/api/v1/modules/a76/general_catalogs/company/routes.py index 0095bc4d..2d967bb0 100644 --- a/backend/api/v1/modules/a76/general_catalogs/company/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/company/routes.py @@ -9,15 +9,29 @@ from sqlalchemy.orm import Session from core.database import get_core_db from core.security import get_current_user, validate_access_to_resource -from .....common.tenant_crud_routes import TenantCRUDRoutes +from api.v1.common.tenant_crud_routes import TenantCRUDRoutes from .dto import CompanyCreateDTO, CompanyResponseDTO, CompanyUpdateDTO from .models import Company from .service import CompanyService -# Main router that includes base CRUD -router = APIRouter(prefix="/company") +# Create CRUD router +crud_router = TenantCRUDRoutes( + service=CompanyService, + create_schema=CompanyCreateDTO, + update_schema=CompanyUpdateDTO, + response_schema=CompanyResponseDTO, + prefix="/company", + tags=["a76.general_catalogs.company"], + resource_name="Company", + enable_list=True, + enable_filters=True, +).router + +router = crud_router # Custom endpoints + + @router.get( "/my-companies", response_model=List[CompanyResponseDTO], @@ -83,7 +97,8 @@ async def get_basic_info( db, tenant_id, company_id_from_user, Company, company_id, "id" ) - company = CompanyService.get_by_id(db, company_id, tenant_id, company_id_from_user) + company = CompanyService.get_by_id( + db, company_id, tenant_id, company_id_from_user) if not company: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -117,7 +132,8 @@ async def get_responsible_info( db, tenant_id, company_id_from_user, Company, company_id, "id" ) - company = CompanyService.get_by_id(db, company_id, tenant_id, company_id_from_user) + company = CompanyService.get_by_id( + db, company_id, tenant_id, company_id_from_user) if not company: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -153,7 +169,8 @@ async def get_program_info( db, tenant_id, company_id_from_user, Company, company_id, "id" ) - company = CompanyService.get_by_id(db, company_id, tenant_id, company_id_from_user) + company = CompanyService.get_by_id( + db, company_id, tenant_id, company_id_from_user) if not company: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -166,7 +183,7 @@ async def get_program_info( "prosec": company.prosec, "prosec_authorization": company.prosec_authorization, } - + # Base CRUD routes using TenantCRUDRoutes base_router = TenantCRUDRoutes( service=CompanyService, @@ -179,4 +196,4 @@ base_router = TenantCRUDRoutes( enable_list=True, enable_filters=True, ).router -router.include_router(base_router) \ No newline at end of file +router.include_router(base_router) diff --git a/backend/api/v1/modules/a76/general_catalogs/concepts/models.py b/backend/api/v1/modules/a76/general_catalogs/concepts/models.py index ec20fb82..8c13fc34 100644 --- a/backend/api/v1/modules/a76/general_catalogs/concepts/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/concepts/models.py @@ -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): diff --git a/backend/api/v1/modules/a76/general_catalogs/concepts/routes.py b/backend/api/v1/modules/a76/general_catalogs/concepts/routes.py index 46975d31..f579cef0 100644 --- a/backend/api/v1/modules/a76/general_catalogs/concepts/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/concepts/routes.py @@ -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 diff --git a/backend/api/v1/modules/a76/general_catalogs/concepts/service.py b/backend/api/v1/modules/a76/general_catalogs/concepts/service.py index d3526068..bba0de63 100644 --- a/backend/api/v1/modules/a76/general_catalogs/concepts/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/concepts/service.py @@ -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 diff --git a/backend/api/v1/modules/a76/general_catalogs/customs_broker_concepts/routes.py b/backend/api/v1/modules/a76/general_catalogs/customs_broker_concepts/routes.py index d47379ef..eeb5bfab 100644 --- a/backend/api/v1/modules/a76/general_catalogs/customs_broker_concepts/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/customs_broker_concepts/routes.py @@ -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 diff --git a/backend/api/v1/modules/a76/general_catalogs/customs_broker_concepts/service.py b/backend/api/v1/modules/a76/general_catalogs/customs_broker_concepts/service.py index 8adf190a..43e7d1d8 100644 --- a/backend/api/v1/modules/a76/general_catalogs/customs_broker_concepts/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/customs_broker_concepts/service.py @@ -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 diff --git a/backend/api/v1/modules/a76/general_catalogs/doda/routes.py b/backend/api/v1/modules/a76/general_catalogs/doda/routes.py index e06e0ea3..b687bdfd 100644 --- a/backend/api/v1/modules/a76/general_catalogs/doda/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/doda/routes.py @@ -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, diff --git a/backend/api/v1/modules/a76/general_catalogs/doda/service.py b/backend/api/v1/modules/a76/general_catalogs/doda/service.py index a33a096c..43690982 100644 --- a/backend/api/v1/modules/a76/general_catalogs/doda/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/doda/service.py @@ -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() ) diff --git a/backend/api/v1/modules/a76/general_catalogs/electronic_notices/routes.py b/backend/api/v1/modules/a76/general_catalogs/electronic_notices/routes.py index 7d1a19e3..576f4cf0 100644 --- a/backend/api/v1/modules/a76/general_catalogs/electronic_notices/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/electronic_notices/routes.py @@ -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] diff --git a/backend/api/v1/modules/a76/general_catalogs/electronic_notices/service.py b/backend/api/v1/modules/a76/general_catalogs/electronic_notices/service.py index e357fb78..0fadd40f 100644 --- a/backend/api/v1/modules/a76/general_catalogs/electronic_notices/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/electronic_notices/service.py @@ -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() ) diff --git a/backend/api/v1/modules/a76/general_catalogs/equivalencies/models.py b/backend/api/v1/modules/a76/general_catalogs/equivalencies/models.py index 9fa8f1df..7bcc724a 100644 --- a/backend/api/v1/modules/a76/general_catalogs/equivalencies/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/equivalencies/models.py @@ -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") diff --git a/backend/api/v1/modules/a76/general_catalogs/equivalencies/routes.py b/backend/api/v1/modules/a76/general_catalogs/equivalencies/routes.py index 709b9f4e..d7c1c42f 100644 --- a/backend/api/v1/modules/a76/general_catalogs/equivalencies/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/equivalencies/routes.py @@ -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) diff --git a/backend/api/v1/modules/a76/general_catalogs/equivalencies/service.py b/backend/api/v1/modules/a76/general_catalogs/equivalencies/service.py index e661fdc9..16a626f9 100644 --- a/backend/api/v1/modules/a76/general_catalogs/equivalencies/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/equivalencies/service.py @@ -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 diff --git a/backend/api/v1/modules/a76/general_catalogs/error_catalogs/routes.py b/backend/api/v1/modules/a76/general_catalogs/error_catalogs/routes.py index 1116b5a5..97af7429 100644 --- a/backend/api/v1/modules/a76/general_catalogs/error_catalogs/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/error_catalogs/routes.py @@ -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) diff --git a/backend/api/v1/modules/a76/general_catalogs/error_catalogs/service.py b/backend/api/v1/modules/a76/general_catalogs/error_catalogs/service.py index 8cddc486..06ebf664 100644 --- a/backend/api/v1/modules/a76/general_catalogs/error_catalogs/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/error_catalogs/service.py @@ -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" + ) diff --git a/backend/api/v1/modules/a76/general_catalogs/exchange_rate/services.py b/backend/api/v1/modules/a76/general_catalogs/exchange_rate/services.py index bbbe6f61..469b212f 100644 --- a/backend/api/v1/modules/a76/general_catalogs/exchange_rate/services.py +++ b/backend/api/v1/modules/a76/general_catalogs/exchange_rate/services.py @@ -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( diff --git a/backend/api/v1/modules/a76/general_catalogs/identifiers/routes.py b/backend/api/v1/modules/a76/general_catalogs/identifiers/routes.py index 8fdb2ce1..26d5fb3e 100644 --- a/backend/api/v1/modules/a76/general_catalogs/identifiers/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/identifiers/routes.py @@ -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) diff --git a/backend/api/v1/modules/a76/general_catalogs/identifiers/service.py b/backend/api/v1/modules/a76/general_catalogs/identifiers/service.py index d7b19800..7aecde17 100644 --- a/backend/api/v1/modules/a76/general_catalogs/identifiers/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/identifiers/service.py @@ -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 diff --git a/backend/api/v1/modules/a76/general_catalogs/inpc/models.py b/backend/api/v1/modules/a76/general_catalogs/inpc/models.py index caedb772..7a41d26b 100644 --- a/backend/api/v1/modules/a76/general_catalogs/inpc/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/inpc/models.py @@ -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"} ) diff --git a/backend/api/v1/modules/a76/general_catalogs/inpc/routes.py b/backend/api/v1/modules/a76/general_catalogs/inpc/routes.py index d02fbcaf..dd2b6cdb 100644 --- a/backend/api/v1/modules/a76/general_catalogs/inpc/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/inpc/routes.py @@ -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) diff --git a/backend/api/v1/modules/a76/general_catalogs/inpc/service.py b/backend/api/v1/modules/a76/general_catalogs/inpc/service.py index c67d4cf4..6336ea35 100644 --- a/backend/api/v1/modules/a76/general_catalogs/inpc/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/inpc/service.py @@ -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 diff --git a/backend/api/v1/modules/a76/general_catalogs/legends/models.py b/backend/api/v1/modules/a76/general_catalogs/legends/models.py index 65b1658b..eaea60a1 100644 --- a/backend/api/v1/modules/a76/general_catalogs/legends/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/legends/models.py @@ -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"} ) diff --git a/backend/api/v1/modules/a76/general_catalogs/legends/routes.py b/backend/api/v1/modules/a76/general_catalogs/legends/routes.py index ebe22fb5..e053c4d5 100644 --- a/backend/api/v1/modules/a76/general_catalogs/legends/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/legends/routes.py @@ -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) diff --git a/backend/api/v1/modules/a76/general_catalogs/legends/service.py b/backend/api/v1/modules/a76/general_catalogs/legends/service.py index 153b11cf..0221ecbb 100644 --- a/backend/api/v1/modules/a76/general_catalogs/legends/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/legends/service.py @@ -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 diff --git a/backend/api/v1/modules/a76/general_catalogs/multi_currency_types/models.py b/backend/api/v1/modules/a76/general_catalogs/multi_currency_types/models.py index 0163b8af..91751a9a 100644 --- a/backend/api/v1/modules/a76/general_catalogs/multi_currency_types/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/multi_currency_types/models.py @@ -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"} ) diff --git a/backend/api/v1/modules/a76/general_catalogs/multi_currency_types/routes.py b/backend/api/v1/modules/a76/general_catalogs/multi_currency_types/routes.py index 38ff42ab..96f26034 100644 --- a/backend/api/v1/modules/a76/general_catalogs/multi_currency_types/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/multi_currency_types/routes.py @@ -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) diff --git a/backend/api/v1/modules/a76/general_catalogs/multi_currency_types/service.py b/backend/api/v1/modules/a76/general_catalogs/multi_currency_types/service.py index 8c315f2c..979ff3d3 100644 --- a/backend/api/v1/modules/a76/general_catalogs/multi_currency_types/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/multi_currency_types/service.py @@ -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 diff --git a/backend/api/v1/modules/a76/general_catalogs/packages/services.py b/backend/api/v1/modules/a76/general_catalogs/packages/services.py index 9a732972..be52f4f7 100644 --- a/backend/api/v1/modules/a76/general_catalogs/packages/services.py +++ b/backend/api/v1/modules/a76/general_catalogs/packages/services.py @@ -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 diff --git a/backend/api/v1/modules/a76/general_catalogs/ports/models.py b/backend/api/v1/modules/a76/general_catalogs/ports/models.py index daf4993c..01aa45df 100644 --- a/backend/api/v1/modules/a76/general_catalogs/ports/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/ports/models.py @@ -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"} ) diff --git a/backend/api/v1/modules/a76/general_catalogs/ports/routes.py b/backend/api/v1/modules/a76/general_catalogs/ports/routes.py index eb7ce288..3d03c33d 100644 --- a/backend/api/v1/modules/a76/general_catalogs/ports/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/ports/routes.py @@ -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 diff --git a/backend/api/v1/modules/a76/general_catalogs/ports/service.py b/backend/api/v1/modules/a76/general_catalogs/ports/service.py index b0db38fb..54b41df2 100644 --- a/backend/api/v1/modules/a76/general_catalogs/ports/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/ports/service.py @@ -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 diff --git a/backend/api/v1/modules/a76/general_catalogs/prevalidators/models.py b/backend/api/v1/modules/a76/general_catalogs/prevalidators/models.py index 35ec9f11..aab8dc00 100644 --- a/backend/api/v1/modules/a76/general_catalogs/prevalidators/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/prevalidators/models.py @@ -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)) diff --git a/backend/api/v1/modules/a76/general_catalogs/prevalidators/routes.py b/backend/api/v1/modules/a76/general_catalogs/prevalidators/routes.py index e5b12f07..71ee0120 100644 --- a/backend/api/v1/modules/a76/general_catalogs/prevalidators/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/prevalidators/routes.py @@ -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), diff --git a/backend/api/v1/modules/a76/general_catalogs/prevalidators/service.py b/backend/api/v1/modules/a76/general_catalogs/prevalidators/service.py index 60924ea1..a6bffe39 100644 --- a/backend/api/v1/modules/a76/general_catalogs/prevalidators/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/prevalidators/service.py @@ -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() ) diff --git a/backend/api/v1/modules/a76/general_catalogs/seal/models.py b/backend/api/v1/modules/a76/general_catalogs/seal/models.py index 623d4580..2e254b40 100644 --- a/backend/api/v1/modules/a76/general_catalogs/seal/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/seal/models.py @@ -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"}, ) diff --git a/backend/api/v1/modules/a76/general_catalogs/signatures/models.py b/backend/api/v1/modules/a76/general_catalogs/signatures/models.py index b5702dea..e3d04b63 100644 --- a/backend/api/v1/modules/a76/general_catalogs/signatures/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/signatures/models.py @@ -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)) diff --git a/backend/api/v1/modules/a76/general_catalogs/signatures/routes.py b/backend/api/v1/modules/a76/general_catalogs/signatures/routes.py index 9ac77be0..f7c390a5 100644 --- a/backend/api/v1/modules/a76/general_catalogs/signatures/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/signatures/routes.py @@ -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) diff --git a/backend/api/v1/modules/a76/general_catalogs/signatures/service.py b/backend/api/v1/modules/a76/general_catalogs/signatures/service.py index f278a635..6b01bcf5 100644 --- a/backend/api/v1/modules/a76/general_catalogs/signatures/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/signatures/service.py @@ -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 diff --git a/backend/api/v1/modules/a76/general_catalogs/unit_conversions/models.py b/backend/api/v1/modules/a76/general_catalogs/unit_conversions/models.py index 32de818d..65122a4c 100644 --- a/backend/api/v1/modules/a76/general_catalogs/unit_conversions/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/unit_conversions/models.py @@ -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 diff --git a/backend/api/v1/modules/a76/general_catalogs/unit_conversions/routes.py b/backend/api/v1/modules/a76/general_catalogs/unit_conversions/routes.py index e44a6a70..caf14f2b 100644 --- a/backend/api/v1/modules/a76/general_catalogs/unit_conversions/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/unit_conversions/routes.py @@ -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 diff --git a/backend/api/v1/modules/a76/general_catalogs/unit_conversions/service.py b/backend/api/v1/modules/a76/general_catalogs/unit_conversions/service.py index e9a505ea..11c447be 100644 --- a/backend/api/v1/modules/a76/general_catalogs/unit_conversions/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/unit_conversions/service.py @@ -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 diff --git a/backend/api/v1/modules/a76/general_catalogs/units_of_measure/models.py b/backend/api/v1/modules/a76/general_catalogs/units_of_measure/models.py index 704045aa..04cae220 100644 --- a/backend/api/v1/modules/a76/general_catalogs/units_of_measure/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/units_of_measure/models.py @@ -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() diff --git a/backend/api/v1/modules/a76/general_catalogs/units_of_measure/routes.py b/backend/api/v1/modules/a76/general_catalogs/units_of_measure/routes.py index 56c2dc6d..9a9b3db7 100644 --- a/backend/api/v1/modules/a76/general_catalogs/units_of_measure/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/units_of_measure/routes.py @@ -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) diff --git a/backend/api/v1/modules/a76/general_catalogs/units_of_measure/service.py b/backend/api/v1/modules/a76/general_catalogs/units_of_measure/service.py index aa8d7e3b..b21faa7a 100644 --- a/backend/api/v1/modules/a76/general_catalogs/units_of_measure/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/units_of_measure/service.py @@ -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) diff --git a/backend/api/v1/modules/a76/parts/models.py b/backend/api/v1/modules/a76/parts/models.py index c1bd8403..a536d897 100644 --- a/backend/api/v1/modules/a76/parts/models.py +++ b/backend/api/v1/modules/a76/parts/models.py @@ -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)) diff --git a/backend/api/v1/modules/a76/router.py b/backend/api/v1/modules/a76/router.py index be3a8b78..9729c23d 100644 --- a/backend/api/v1/modules/a76/router.py +++ b/backend/api/v1/modules/a76/router.py @@ -15,13 +15,29 @@ 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 .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 .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 .tenants import router as tenants_router from .transportation.trailers.routes import router as trailers_router from .transportation.transporters.routes import router as transporters_router @@ -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") diff --git a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/models.py b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/models.py index 0593cafe..9896d4e7 100644 --- a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/models.py +++ b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/models.py @@ -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 diff --git a/backend/api/v1/modules/public/reference_data/containers/models.py b/backend/api/v1/modules/public/reference_data/containers/models.py index 2db64a49..b8d3c630 100644 --- a/backend/api/v1/modules/public/reference_data/containers/models.py +++ b/backend/api/v1/modules/public/reference_data/containers/models.py @@ -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( diff --git a/backend/api/v1/modules/public/reference_data/countries/models.py b/backend/api/v1/modules/public/reference_data/countries/models.py index 483ac234..7ddcf47c 100644 --- a/backend/api/v1/modules/public/reference_data/countries/models.py +++ b/backend/api/v1/modules/public/reference_data/countries/models.py @@ -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 diff --git a/backend/api/v1/modules/public/reference_data/currency_types/models.py b/backend/api/v1/modules/public/reference_data/currency_types/models.py index 53f24b15..b4b507ea 100644 --- a/backend/api/v1/modules/public/reference_data/currency_types/models.py +++ b/backend/api/v1/modules/public/reference_data/currency_types/models.py @@ -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 diff --git a/backend/api/v1/modules/public/reference_data/customs_sections/models.py b/backend/api/v1/modules/public/reference_data/customs_sections/models.py index 90d3e18d..011967b7 100644 --- a/backend/api/v1/modules/public/reference_data/customs_sections/models.py +++ b/backend/api/v1/modules/public/reference_data/customs_sections/models.py @@ -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) diff --git a/backend/api/v1/modules/public/reference_data/customs_warehouses/models.py b/backend/api/v1/modules/public/reference_data/customs_warehouses/models.py index 80dd65d9..f6c54d05 100644 --- a/backend/api/v1/modules/public/reference_data/customs_warehouses/models.py +++ b/backend/api/v1/modules/public/reference_data/customs_warehouses/models.py @@ -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) diff --git a/backend/api/v1/modules/public/reference_data/incoterms/models.py b/backend/api/v1/modules/public/reference_data/incoterms/models.py index 7954fa9d..65028280 100644 --- a/backend/api/v1/modules/public/reference_data/incoterms/models.py +++ b/backend/api/v1/modules/public/reference_data/incoterms/models.py @@ -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) diff --git a/backend/api/v1/modules/public/reference_data/invoice_types/models.py b/backend/api/v1/modules/public/reference_data/invoice_types/models.py index f8b04633..8b3671f0 100644 --- a/backend/api/v1/modules/public/reference_data/invoice_types/models.py +++ b/backend/api/v1/modules/public/reference_data/invoice_types/models.py @@ -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): diff --git a/backend/api/v1/modules/public/reference_data/material_types/models.py b/backend/api/v1/modules/public/reference_data/material_types/models.py index 25b27f3f..04fa9478 100644 --- a/backend/api/v1/modules/public/reference_data/material_types/models.py +++ b/backend/api/v1/modules/public/reference_data/material_types/models.py @@ -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 diff --git a/backend/api/v1/modules/public/reference_data/payment_methods/models.py b/backend/api/v1/modules/public/reference_data/payment_methods/models.py index 174019db..35870726 100644 --- a/backend/api/v1/modules/public/reference_data/payment_methods/models.py +++ b/backend/api/v1/modules/public/reference_data/payment_methods/models.py @@ -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) diff --git a/backend/api/v1/modules/public/reference_data/pedimento_codes/models.py b/backend/api/v1/modules/public/reference_data/pedimento_codes/models.py index ee5d74f2..691f5117 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_codes/models.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_codes/models.py @@ -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) diff --git a/backend/api/v1/modules/public/reference_data/pedimento_regimens/models.py b/backend/api/v1/modules/public/reference_data/pedimento_regimens/models.py index e24bcc65..a881a7ca 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_regimens/models.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_regimens/models.py @@ -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( diff --git a/backend/api/v1/modules/public/reference_data/sectors/models.py b/backend/api/v1/modules/public/reference_data/sectors/models.py index 426c5357..f87fa0ef 100644 --- a/backend/api/v1/modules/public/reference_data/sectors/models.py +++ b/backend/api/v1/modules/public/reference_data/sectors/models.py @@ -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) diff --git a/backend/api/v1/modules/public/reference_data/states/models.py b/backend/api/v1/modules/public/reference_data/states/models.py index 7a883a31..ce832c98 100644 --- a/backend/api/v1/modules/public/reference_data/states/models.py +++ b/backend/api/v1/modules/public/reference_data/states/models.py @@ -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) diff --git a/backend/api/v1/modules/public/reference_data/transport_modes/models.py b/backend/api/v1/modules/public/reference_data/transport_modes/models.py index 19ae71c5..8eecc40e 100644 --- a/backend/api/v1/modules/public/reference_data/transport_modes/models.py +++ b/backend/api/v1/modules/public/reference_data/transport_modes/models.py @@ -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) diff --git a/backend/api/v1/modules/public/reference_data/transport_types/models.py b/backend/api/v1/modules/public/reference_data/transport_types/models.py index de4bbb3e..0838f66b 100644 --- a/backend/api/v1/modules/public/reference_data/transport_types/models.py +++ b/backend/api/v1/modules/public/reference_data/transport_types/models.py @@ -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( diff --git a/backend/api/v1/modules/public/reference_data/valuation_methods/models.py b/backend/api/v1/modules/public/reference_data/valuation_methods/models.py index b25c9457..cdfe6588 100644 --- a/backend/api/v1/modules/public/reference_data/valuation_methods/models.py +++ b/backend/api/v1/modules/public/reference_data/valuation_methods/models.py @@ -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) diff --git a/backend/core/database.py b/backend/core/database.py index d43e5641..2bd4ba4c 100644 --- a/backend/core/database.py +++ b/backend/core/database.py @@ -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(): diff --git a/frontend/src/lib/api/dashboard/a76/classification-concepts.ts b/frontend/src/lib/api/dashboard/a76/classification-concepts.ts new file mode 100644 index 00000000..6179c188 --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/classification-concepts.ts @@ -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 {} + +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 = {} +): Promise> { + 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 { + const response = await api.get(`/a76/classification_concepts/${id}`); + return response.data; +} + +export async function createClassificationConcept(data: ClassificationConceptCreate): Promise { + const response = await api.post('/a76/classification_concepts', data); + return response.data; +} + +export async function updateClassificationConcept(id: number, data: ClassificationConceptUpdate): Promise { + const response = await api.patch(`/a76/classification_concepts/${id}`, data); + return response.data; +} + +export async function deleteClassificationConcept(id: number): Promise { + await api.delete(`/a76/classification_concepts/${id}`); +} diff --git a/frontend/src/lib/api/dashboard/a76/company.ts b/frontend/src/lib/api/dashboard/a76/company.ts new file mode 100644 index 00000000..3806c386 --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/company.ts @@ -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 = {} +): Promise> { + 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> { + return await api.get(`/a76/company/${id}`); +} + +export async function createCompany(data: CompanyCreate): Promise> { + return await api.post(`/a76/company`, data); +} + +export async function updateCompany(id: number, data: CompanyUpdate): Promise> { + return await api.put(`/a76/company/${id}`, data); +} + +export async function deleteCompany(id: number): Promise> { + return await api.delete(`/a76/company/${id}`); +} diff --git a/frontend/src/lib/api/dashboard/a76/concepts.ts b/frontend/src/lib/api/dashboard/a76/concepts.ts new file mode 100644 index 00000000..aa723d72 --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/concepts.ts @@ -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 {} + +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 = {} +): Promise> { + 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 { + const response = await api.get(`/a76/concepts/${id}`); + return response.data; +} + +export async function createConcept(data: ConceptCreate): Promise { + const response = await api.post('/a76/concepts', data); + return response.data; +} + +export async function updateConcept(id: number, data: ConceptUpdate): Promise { + const response = await api.patch(`/a76/concepts/${id}`, data); + return response.data; +} + +export async function deleteConcept(id: number): Promise { + await api.delete(`/a76/concepts/${id}`); +} diff --git a/frontend/src/lib/api/dashboard/a76/customs-broker-concepts.ts b/frontend/src/lib/api/dashboard/a76/customs-broker-concepts.ts new file mode 100644 index 00000000..5e296fb8 --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/customs-broker-concepts.ts @@ -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 {} + +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 = {} +): Promise> { + 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 { + const response = await api.get(`/a76/customs_broker_concepts/${id}`); + return response.data; +} + +export async function createCustomsBrokerConcept(data: CustomsBrokerConceptCreate): Promise { + const response = await api.post('/a76/customs_broker_concepts', data); + return response.data; +} + +export async function updateCustomsBrokerConcept(id: number, data: CustomsBrokerConceptUpdate): Promise { + const response = await api.patch(`/a76/customs_broker_concepts/${id}`, data); + return response.data; +} + +export async function deleteCustomsBrokerConcept(id: number): Promise { + await api.delete(`/a76/customs_broker_concepts/${id}`); +} diff --git a/frontend/src/lib/api/dashboard/a76/doda.ts b/frontend/src/lib/api/dashboard/a76/doda.ts new file mode 100644 index 00000000..74ce4983 --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/doda.ts @@ -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 {} + +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 = {} +): Promise> { + 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 { + const response = await api.get(`/a76/doda/${id}`); + return response.data; +} + +export async function createDODA(data: DODACreate): Promise { + const response = await api.post('/a76/doda', data); + return response.data; +} + +export async function updateDODA(id: number, data: DODAUpdate): Promise { + const response = await api.patch(`/a76/doda/${id}`, data); + return response.data; +} + +export async function deleteDODA(id: number): Promise { + await api.delete(`/a76/doda/${id}`); +} diff --git a/frontend/src/lib/api/dashboard/a76/electronic-notices.ts b/frontend/src/lib/api/dashboard/a76/electronic-notices.ts new file mode 100644 index 00000000..a1bb341f --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/electronic-notices.ts @@ -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 {} + +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 = {} +): Promise> { + 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 { + const response = await api.get(`/a76/electronic_notices/${id}`); + return response.data; +} + +export async function createElectronicNotice(data: ElectronicNoticeCreate): Promise { + const response = await api.post('/a76/electronic_notices', data); + return response.data; +} + +export async function updateElectronicNotice(id: number, data: ElectronicNoticeUpdate): Promise { + const response = await api.patch(`/a76/electronic_notices/${id}`, data); + return response.data; +} + +export async function deleteElectronicNotice(id: number): Promise { + await api.delete(`/a76/electronic_notices/${id}`); +} diff --git a/frontend/src/lib/api/dashboard/a76/equivalencies.ts b/frontend/src/lib/api/dashboard/a76/equivalencies.ts new file mode 100644 index 00000000..afed400c --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/equivalencies.ts @@ -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 {} + +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 = {} +): Promise> { + 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 { + const response = await api.get(`/a76/equivalencies/${id}`); + return response.data; +} + +export async function createEquivalency(data: EquivalencyCreate): Promise { + const response = await api.post('/a76/equivalencies', data); + return response.data; +} + +export async function updateEquivalency(id: number, data: EquivalencyUpdate): Promise { + const response = await api.patch(`/a76/equivalencies/${id}`, data); + return response.data; +} + +export async function deleteEquivalency(id: number): Promise { + await api.delete(`/a76/equivalencies/${id}`); +} diff --git a/frontend/src/lib/api/dashboard/a76/error-catalogs.ts b/frontend/src/lib/api/dashboard/a76/error-catalogs.ts new file mode 100644 index 00000000..994caa7f --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/error-catalogs.ts @@ -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 {} + +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 = {} +): Promise> { + 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 { + const response = await api.get(`/a76/error_catalogs/${id}`); + return response.data; +} + +export async function createErrorCatalog(data: ErrorCatalogCreate): Promise { + const response = await api.post('/a76/error_catalogs', data); + return response.data; +} + +export async function updateErrorCatalog(id: number, data: ErrorCatalogUpdate): Promise { + const response = await api.patch(`/a76/error_catalogs/${id}`, data); + return response.data; +} + +export async function deleteErrorCatalog(id: number): Promise { + await api.delete(`/a76/error_catalogs/${id}`); +} diff --git a/frontend/src/lib/api/dashboard/a76/identifiers.ts b/frontend/src/lib/api/dashboard/a76/identifiers.ts new file mode 100644 index 00000000..5a2ca8d3 --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/identifiers.ts @@ -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 = {} +): Promise> { + 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> { + return await api.post('/a76/identifiers', data); +} + +export async function updateIdentifier(id: number, data: IdentifierUpdate): Promise> { + return await api.put(`/a76/identifiers/${id}`, data); +} + +export async function deleteIdentifier(id: number): Promise> { + return await api.delete(`/a76/identifiers/${id}`); +} diff --git a/frontend/src/lib/api/dashboard/a76/index.ts b/frontend/src/lib/api/dashboard/a76/index.ts index 84029759..5b539085 100644 --- a/frontend/src/lib/api/dashboard/a76/index.ts +++ b/frontend/src/lib/api/dashboard/a76/index.ts @@ -4,3 +4,16 @@ export * from './classes'; export * from './packages'; export * from './exchange-rate'; +export * from './concepts'; +export * from './customs-broker-concepts'; +export * from './classification-concepts'; +export * from './unit-conversions'; +export * from './equivalencies'; +export * from './multi-currency-types'; +export * from './inpc'; +export * from './legends'; +export * from './signatures'; +export * from './error-catalogs'; +export * from './doda'; +export * from './prevalidators'; +export * from './electronic-notices'; diff --git a/frontend/src/lib/api/dashboard/a76/inpc.ts b/frontend/src/lib/api/dashboard/a76/inpc.ts new file mode 100644 index 00000000..95f5d8c4 --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/inpc.ts @@ -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 {} + +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 = {} +): Promise> { + 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 { + const response = await api.get(`/a76/inpc/${id}`); + return response.data; +} + +export async function createINPC(data: INPCCreate): Promise { + const response = await api.post('/a76/inpc', data); + return response.data; +} + +export async function updateINPC(id: number, data: INPCUpdate): Promise { + const response = await api.patch(`/a76/inpc/${id}`, data); + return response.data; +} + +export async function deleteINPC(id: number): Promise { + await api.delete(`/a76/inpc/${id}`); +} diff --git a/frontend/src/lib/api/dashboard/a76/legends.ts b/frontend/src/lib/api/dashboard/a76/legends.ts new file mode 100644 index 00000000..d04317a6 --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/legends.ts @@ -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 {} + +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 = {} +): Promise> { + 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 { + const response = await api.get(`/a76/legends/${id}`); + return response.data; +} + +export async function createLegend(data: LegendCreate): Promise { + const response = await api.post('/a76/legends', data); + return response.data; +} + +export async function updateLegend(id: number, data: LegendUpdate): Promise { + const response = await api.patch(`/a76/legends/${id}`, data); + return response.data; +} + +export async function deleteLegend(id: number): Promise { + await api.delete(`/a76/legends/${id}`); +} diff --git a/frontend/src/lib/api/dashboard/a76/multi-currency-types.ts b/frontend/src/lib/api/dashboard/a76/multi-currency-types.ts new file mode 100644 index 00000000..d944b8c7 --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/multi-currency-types.ts @@ -0,0 +1,61 @@ +import { api } from '$lib/api'; +import type { ApiResponse } from '$lib/api'; + +export interface MultiCurrencyType { + id: number; + key: string; + description?: string; + tenant_id: string; + company_id?: string; + created_at: string; + updated_at?: string; +} + +export interface MultiCurrencyTypeCreate { + key: string; + description?: string; +} + +export interface MultiCurrencyTypeUpdate extends Partial {} + +export interface MultiCurrencyTypeListResponse { + items: MultiCurrencyType[]; + total: number; + page: number; + page_size: number; + pages: number; +} + +export async function getMultiCurrencyTypes( + page: number = 1, + pageSize: number = 50, + filters: Record = {} +): Promise> { + const params = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + ...filters + }); + + const response = await api.get(`/a76/multi_currency_types?${params.toString()}`); + return response.data; +} + +export async function getMultiCurrencyType(id: number): Promise { + const response = await api.get(`/a76/multi_currency_types/${id}`); + return response.data; +} + +export async function createMultiCurrencyType(data: MultiCurrencyTypeCreate): Promise { + const response = await api.post('/a76/multi_currency_types', data); + return response.data; +} + +export async function updateMultiCurrencyType(id: number, data: MultiCurrencyTypeUpdate): Promise { + const response = await api.patch(`/a76/multi_currency_types/${id}`, data); + return response.data; +} + +export async function deleteMultiCurrencyType(id: number): Promise { + await api.delete(`/a76/multi_currency_types/${id}`); +} diff --git a/frontend/src/lib/api/dashboard/a76/packages.ts b/frontend/src/lib/api/dashboard/a76/packages.ts index 8fb8d682..7af320b2 100644 --- a/frontend/src/lib/api/dashboard/a76/packages.ts +++ b/frontend/src/lib/api/dashboard/a76/packages.ts @@ -1,122 +1,78 @@ -/** - * API para gestión de Packages (Bultos/Embalajes A76) - */ import { api } from '$lib/api'; import type { ApiResponse } from '$lib/api'; export interface Package { - id: number; - tenant_id: number; - company_id: number; - key: string; - description_es: string | null; - description_en: string | null; - weight_unit: number | null; - plurals: string | null; - plural_in: string | null; - code_ace: string | null; - code_aamex: string | null; - created_at: string | null; - updated_at: string | null; + id: number; + tenant_id: number; + company_id: number; + key: string; + description_es: string | null; + description_en: string | null; + weight_unit: number | null; + plurals: string | null; + plural_in: string | null; + code_ace: string | null; + code_aamex: string | null; + created_at: string | null; + updated_at: string | null; } export interface PackageCreate { - key: string; - description_es?: string | null; - description_en?: string | null; - weight_unit?: number | null; - plurals?: string | null; - plural_in?: string | null; - code_ace?: string | null; - code_aamex?: string | null; + key: string; + description_es?: string | null; + description_en?: string | null; + weight_unit?: number | null; + plurals?: string | null; + plural_in?: string | null; + code_ace?: string | null; + code_aamex?: string | null; + company_id: number; } export interface PackageUpdate { - key?: string; - description_es?: string | null; - description_en?: string | null; - weight_unit?: number | null; - plurals?: string | null; - plural_in?: string | null; - code_ace?: string | null; - code_aamex?: string | null; + key?: string; + description_es?: string | null; + description_en?: string | null; + weight_unit?: number | null; + plurals?: string | null; + plural_in?: string | null; + code_ace?: string | null; + code_aamex?: string | null; } export interface PackageListResponse { - items: Package[]; - total: number; - page: number; - page_size: number; - pages: number; + items: Package[]; + total: number; + page: number; + page_size: number; + pages: number; } -export interface PackageFilters { - key?: string; - description_es?: string; -} - -/** - * Obtener lista de packages con paginación - */ export async function getPackages( - companyId: number, - page: number = 1, - pageSize: number = 50, - filters?: PackageFilters + page = 1, + pageSize = 50, + filters: Record = {} ): Promise> { - const params = new URLSearchParams({ - company_id: companyId.toString(), - page: page.toString(), - page_size: pageSize.toString() - }); - - if (filters?.key) { - params.append('key', filters.key); - } - if (filters?.description_es) { - params.append('description_es', filters.description_es); - } - - return api.get(`/v1/a76/packages/?${params.toString()}`); + const queryParams = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + ...filters + }); + return await api.get(`/a76/packages?${queryParams.toString()}`); } -/** - * Obtener un package por ID - */ -export async function getPackage( - packageId: number, - companyId: number -): Promise> { - return api.get(`/v1/a76/packages/${packageId}?company_id=${companyId}`); +export async function getPackage(id: number): Promise> { + return await api.get(`/a76/packages/${id}`); } -/** - * Crear un nuevo package - */ -export async function createPackage( - data: PackageCreate, - companyId: number -): Promise> { - return api.post(`/v1/a76/packages/?company_id=${companyId}`, data); +export async function createPackage(data: PackageCreate): Promise> { + return await api.post(`/a76/packages`, data); } -/** - * Actualizar un package existente - */ -export async function updatePackage( - packageId: number, - data: PackageUpdate, - companyId: number -): Promise> { - return api.put(`/v1/a76/packages/${packageId}?company_id=${companyId}`, data); +export async function updatePackage(id: number, data: PackageUpdate): Promise> { + return await api.put(`/a76/packages/${id}`, data); } -/** - * Eliminar un package - */ -export async function deletePackage( - packageId: number, - companyId: number -): Promise> { - return api.delete(`/v1/a76/packages/${packageId}?company_id=${companyId}`); +export async function deletePackage(id: number): Promise> { + return await api.delete(`/a76/packages/${id}`); } diff --git a/frontend/src/lib/api/dashboard/a76/ports.ts b/frontend/src/lib/api/dashboard/a76/ports.ts new file mode 100644 index 00000000..932c4db1 --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/ports.ts @@ -0,0 +1,68 @@ +import { api } from '$lib/api'; +import type { ApiResponse } from '$lib/api'; + +export enum PortType { + ENTRY = 'ENTRY', + EXIT = 'EXIT', + BOTH = 'BOTH' +} + +export interface Port { + id: number; + port_code: string; + description: string | null; + location_code: string; + location_description: string | null; + port_type: PortType; + created_at: string | null; + updated_at: string | null; +} + +export interface PortCreate { + port_code: string; + description?: string | null; + location_code: string; + location_description?: string | null; + port_type?: PortType; +} + +export interface PortUpdate { + port_code?: string; + description?: string | null; + location_code?: string; + location_description?: string | null; + port_type?: PortType; +} + +export interface PortListResponse { + items: Port[]; + total: number; + page: number; + page_size: number; + pages: number; +} + +export async function getPorts( + page = 1, + pageSize = 50, + filters: Record = {} +): Promise> { + const queryParams = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + ...filters + }); + return await api.get(`/a76/ports?${queryParams.toString()}`); +} + +export async function createPort(data: PortCreate): Promise> { + return await api.post('/a76/ports', data); +} + +export async function updatePort(id: number, data: PortUpdate): Promise> { + return await api.put(`/a76/ports/${id}`, data); +} + +export async function deletePort(id: number): Promise> { + return await api.delete(`/a76/ports/${id}`); +} diff --git a/frontend/src/lib/api/dashboard/a76/prevalidators.ts b/frontend/src/lib/api/dashboard/a76/prevalidators.ts new file mode 100644 index 00000000..fde1a562 --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/prevalidators.ts @@ -0,0 +1,61 @@ +import { api } from '$lib/api'; +import type { ApiResponse } from '$lib/api'; + +export interface Prevalidator { + id: number; + code: string; + description?: string; + tenant_id: string; + company_id?: string; + created_at: string; + updated_at?: string; +} + +export interface PrevalidatorCreate { + code: string; + description?: string; +} + +export interface PrevalidatorUpdate extends Partial {} + +export interface PrevalidatorListResponse { + items: Prevalidator[]; + total: number; + page: number; + page_size: number; + pages: number; +} + +export async function getPrevalidators( + page: number = 1, + pageSize: number = 50, + filters: Record = {} +): Promise> { + const params = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + ...filters + }); + + const response = await api.get(`/a76/prevalidators?${params.toString()}`); + return response.data; +} + +export async function getPrevalidator(id: number): Promise { + const response = await api.get(`/a76/prevalidators/${id}`); + return response.data; +} + +export async function createPrevalidator(data: PrevalidatorCreate): Promise { + const response = await api.post('/a76/prevalidators', data); + return response.data; +} + +export async function updatePrevalidator(id: number, data: PrevalidatorUpdate): Promise { + const response = await api.patch(`/a76/prevalidators/${id}`, data); + return response.data; +} + +export async function deletePrevalidator(id: number): Promise { + await api.delete(`/a76/prevalidators/${id}`); +} diff --git a/frontend/src/lib/api/dashboard/a76/signatures.ts b/frontend/src/lib/api/dashboard/a76/signatures.ts new file mode 100644 index 00000000..60073763 --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/signatures.ts @@ -0,0 +1,63 @@ +import { api } from '$lib/api'; +import type { ApiResponse } from '$lib/api'; + +export interface Signature { + id: number; + name: string; + position?: string; + certificate?: string; + tenant_id: string; + company_id?: string; + created_at: string; + updated_at?: string; +} + +export interface SignatureCreate { + name: string; + position?: string; + certificate?: string; +} + +export interface SignatureUpdate extends Partial {} + +export interface SignatureListResponse { + items: Signature[]; + total: number; + page: number; + page_size: number; + pages: number; +} + +export async function getSignatures( + page: number = 1, + pageSize: number = 50, + filters: Record = {} +): Promise> { + const params = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + ...filters + }); + + const response = await api.get(`/a76/signatures?${params.toString()}`); + return response.data; +} + +export async function getSignature(id: number): Promise { + const response = await api.get(`/a76/signatures/${id}`); + return response.data; +} + +export async function createSignature(data: SignatureCreate): Promise { + const response = await api.post('/a76/signatures', data); + return response.data; +} + +export async function updateSignature(id: number, data: SignatureUpdate): Promise { + const response = await api.patch(`/a76/signatures/${id}`, data); + return response.data; +} + +export async function deleteSignature(id: number): Promise { + await api.delete(`/a76/signatures/${id}`); +} diff --git a/frontend/src/lib/api/dashboard/a76/unit-conversions.ts b/frontend/src/lib/api/dashboard/a76/unit-conversions.ts new file mode 100644 index 00000000..1494fd76 --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/unit-conversions.ts @@ -0,0 +1,63 @@ +import { api } from '$lib/api'; +import type { ApiResponse } from '$lib/api'; + +export interface UnitConversion { + id: number; + from_unit_id: number; + to_unit_id: number; + conversion_factor: number; + tenant_id: string; + company_id?: string; + created_at: string; + updated_at?: string; +} + +export interface UnitConversionCreate { + from_unit_id: number; + to_unit_id: number; + conversion_factor: number; +} + +export interface UnitConversionUpdate extends Partial {} + +export interface UnitConversionListResponse { + items: UnitConversion[]; + total: number; + page: number; + page_size: number; + pages: number; +} + +export async function getUnitConversions( + page: number = 1, + pageSize: number = 50, + filters: Record = {} +): Promise> { + const params = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + ...filters + }); + + const response = await api.get(`/a76/unit_conversions?${params.toString()}`); + return response.data; +} + +export async function getUnitConversion(id: number): Promise { + const response = await api.get(`/a76/unit_conversions/${id}`); + return response.data; +} + +export async function createUnitConversion(data: UnitConversionCreate): Promise { + const response = await api.post('/a76/unit_conversions', data); + return response.data; +} + +export async function updateUnitConversion(id: number, data: UnitConversionUpdate): Promise { + const response = await api.patch(`/a76/unit_conversions/${id}`, data); + return response.data; +} + +export async function deleteUnitConversion(id: number): Promise { + await api.delete(`/a76/unit_conversions/${id}`); +} diff --git a/frontend/src/lib/api/dashboard/a76/units-of-measure.ts b/frontend/src/lib/api/dashboard/a76/units-of-measure.ts new file mode 100644 index 00000000..cdfa50e4 --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/units-of-measure.ts @@ -0,0 +1,158 @@ +import { api } from '$lib/api'; +import type { ApiResponse } from '$lib/api'; + +// --- ACE --- +export interface UnitOfMeasureACE { + id: number; + code: string; + description: string | null; + created_at: string | null; + updated_at: string | null; +} + +export interface UnitOfMeasureACECreate { + code: string; + description?: string | null; +} + +export interface UnitOfMeasureACEUpdate { + code?: string; + description?: string | null; +} + +export interface UnitOfMeasureACEListResponse { + items: UnitOfMeasureACE[]; + total: number; + page: number; + page_size: number; + pages: number; +} + +export async function getUnitsOfMeasureACE( + page = 1, + pageSize = 50, + filters: Record = {} +): Promise> { + const queryParams = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + ...filters + }); + return await api.get(`/a76/units-of-measure/ace?${queryParams.toString()}`); +} + +export async function createUnitOfMeasureACE(data: UnitOfMeasureACECreate): Promise> { + return await api.post('/a76/units-of-measure/ace', data); +} + +export async function updateUnitOfMeasureACE(id: number, data: UnitOfMeasureACEUpdate): Promise> { + return await api.put(`/a76/units-of-measure/ace/${id}`, data); +} + +export async function deleteUnitOfMeasureACE(id: number): Promise> { + return await api.delete(`/a76/units-of-measure/ace/${id}`); +} + +// --- OMA --- +export interface UnitOfMeasureOMA { + id: number; + code: string; + description: string | null; + created_at: string | null; + updated_at: string | null; +} + +export interface UnitOfMeasureOMACreate { + code: string; + description?: string | null; +} + +export interface UnitOfMeasureOMAUpdate { + code?: string; + description?: string | null; +} + +export interface UnitOfMeasureOMAListResponse { + items: UnitOfMeasureOMA[]; + total: number; + page: number; + page_size: number; + pages: number; +} + +export async function getUnitsOfMeasureOMA( + page = 1, + pageSize = 50, + filters: Record = {} +): Promise> { + const queryParams = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + ...filters + }); + return await api.get(`/a76/units-of-measure/oma?${queryParams.toString()}`); +} + +export async function createUnitOfMeasureOMA(data: UnitOfMeasureOMACreate): Promise> { + return await api.post('/a76/units-of-measure/oma', data); +} + +export async function updateUnitOfMeasureOMA(id: number, data: UnitOfMeasureOMAUpdate): Promise> { + return await api.put(`/a76/units-of-measure/oma/${id}`, data); +} + +export async function deleteUnitOfMeasureOMA(id: number): Promise> { + return await api.delete(`/a76/units-of-measure/oma/${id}`); +} + +// --- American --- +export interface UnitOfMeasureAmerican { + id: number; + code: string; + description: string | null; + created_at: string | null; + updated_at: string | null; +} + +export interface UnitOfMeasureAmericanCreate { + code: string; + description?: string | null; +} + +export interface UnitOfMeasureAmericanUpdate { + code?: string; + description?: string | null; +} + +export interface UnitOfMeasureAmericanListResponse { + items: UnitOfMeasureAmerican[]; + total: number; + page: number; + page_size: number; + pages: number; +} + +export async function getUnitsOfMeasureAmerican( + page = 1, + pageSize = 50, + filters: Record = {} +): Promise> { + const queryParams = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + ...filters + }); + return await api.get(`/a76/units-of-measure/american?${queryParams.toString()}`); +} + +export async function createUnitOfMeasureAmerican(data: UnitOfMeasureAmericanCreate): Promise> { + return await api.post('/a76/units-of-measure/american', data); +} + +export async function updateUnitOfMeasureAmerican(id: number, data: UnitOfMeasureAmericanUpdate): Promise> { + return await api.put(`/a76/units-of-measure/american/${id}`, data); +} + +export async function deleteUnitOfMeasureAmerican(id: number): Promise> { + return await api.delete(`/a76/units-of-measure/american/${id}`); +} diff --git a/frontend/src/lib/components/dashboard/company/columns.ts b/frontend/src/lib/components/dashboard/company/columns.ts new file mode 100644 index 00000000..50294b21 --- /dev/null +++ b/frontend/src/lib/components/dashboard/company/columns.ts @@ -0,0 +1,38 @@ +import type { Company } from '$lib/api/dashboard/a76/company'; +import type { ColumnDef } from '@tanstack/table-core'; +import { renderComponent } from '$lib/components/ui/data-table'; +import DataTableActions from './data-table-actions.svelte'; + +export function createColumns(onSuccess?: () => void): ColumnDef[] { + return [ + { + accessorKey: 'name', + header: 'Nombre', + cell: ({ row }) => row.original.name || '-' + }, + { + accessorKey: 'rfc', + header: 'RFC', + cell: ({ row }) => row.original.rfc || '-' + }, + { + accessorKey: 'program', + header: 'Programa', + cell: ({ row }) => row.original.program || '-' + }, + { + accessorKey: 'program_number', + header: 'No. Programa', + cell: ({ row }) => row.original.program_number || '-' + }, + { + id: 'actions', + cell: ({ row }) => { + return renderComponent(DataTableActions, { + item: row.original, + onSuccess + }); + } + } + ]; +} diff --git a/frontend/src/lib/components/dashboard/company/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/company/create-edit-dialog.svelte new file mode 100644 index 00000000..f1e5a5f3 --- /dev/null +++ b/frontend/src/lib/components/dashboard/company/create-edit-dialog.svelte @@ -0,0 +1,127 @@ + + + + + + {title} + + +
{ e.preventDefault(); handleSubmit(); }} class="space-y-4"> + {#if error} +
{error}
+ {/if} + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + + +
+
+
diff --git a/frontend/src/lib/components/dashboard/company/data-table-actions.svelte b/frontend/src/lib/components/dashboard/company/data-table-actions.svelte new file mode 100644 index 00000000..3ca37a3b --- /dev/null +++ b/frontend/src/lib/components/dashboard/company/data-table-actions.svelte @@ -0,0 +1,78 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + + Acciones + dialogOpen = true}> + + Editar + + + + {#if loading} + + {:else} + + {/if} + Eliminar + + + + + diff --git a/frontend/src/lib/components/dashboard/general_catalogs/simple-data-table.svelte b/frontend/src/lib/components/dashboard/general_catalogs/simple-data-table.svelte new file mode 100644 index 00000000..2ebc136d --- /dev/null +++ b/frontend/src/lib/components/dashboard/general_catalogs/simple-data-table.svelte @@ -0,0 +1,93 @@ + + +
+ + + + {#each columns as column (column.key)} + {column.label} + {/each} + + + + {#each data as row, i (i)} + + {#each columns as column (column.key)} + + {getCellValue(row, column)} + + {/each} + + {:else} + + + No hay resultados. + + + {/each} + + +
+ +
+
+ Total: {totalItems} +
+
+ + +
+
diff --git a/frontend/src/lib/components/dashboard/identifiers/columns.ts b/frontend/src/lib/components/dashboard/identifiers/columns.ts new file mode 100644 index 00000000..5feb0f48 --- /dev/null +++ b/frontend/src/lib/components/dashboard/identifiers/columns.ts @@ -0,0 +1,37 @@ +import type { Identifier } from '$lib/api/dashboard/a76/identifiers'; +import type { ColumnDef } from '@tanstack/table-core'; +import { renderComponent } from '$lib/components/ui/data-table'; +import DataTableActions from './data-table-actions.svelte'; + +export function createColumns(onSuccess?: () => void): ColumnDef[] { + return [ + { + accessorKey: 'code', + header: 'Clave', + }, + { + accessorKey: 'description', + header: 'Descripción', + cell: ({ row }) => row.original.description || '-' + }, + { + accessorKey: 'level', + header: 'Nivel', + cell: ({ row }) => row.original.level || '-' + }, + { + accessorKey: 'complement', + header: 'Complemento', + cell: ({ row }) => row.original.complement || '-' + }, + { + id: 'actions', + cell: ({ row }) => { + return renderComponent(DataTableActions, { + item: row.original, + onSuccess + }); + } + } + ]; +} diff --git a/frontend/src/lib/components/dashboard/identifiers/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/identifiers/create-edit-dialog.svelte new file mode 100644 index 00000000..ff5714bd --- /dev/null +++ b/frontend/src/lib/components/dashboard/identifiers/create-edit-dialog.svelte @@ -0,0 +1,139 @@ + + + + + + {title} + + +
+ {#if error} +
{error}
+ {/if} + +
+ + +
+ +
+ +