From e0e38526b0c18c08de8a21415a72732ed2028884 Mon Sep 17 00:00:00 2001 From: acazares Date: Fri, 7 Nov 2025 18:47:41 -0600 Subject: [PATCH] Refactor database models to include company_id and update unique constraints - Updated PermissionRuleOct model to include company_id and modified unique constraint. - Updated Seal model to include company_id and modified unique constraint. - Added TYPE_CHECKING imports in reference data models for better type hinting. - Created new models for QClasses and SClasses in the a24 module. - Enhanced init_first_time.sh script for comprehensive system initialization, including Keycloak and PostgreSQL setup. - Updated documentation to reflect changes in API endpoints and database relationships. --- .../api/v1/modules/a24/q/q_classes/models.py | 32 + .../api/v1/modules/a24/s/s_classes/models.py | 21 + backend/api/v1/modules/a76/classes/dto.py | 8 +- backend/api/v1/modules/a76/classes/models.py | 26 +- backend/api/v1/modules/a76/classes/routes.py | 81 ++- backend/api/v1/modules/a76/classes/service.py | 46 +- .../modules/a76/client_and_provider/models.py | 6 +- backend/api/v1/modules/a76/company/models.py | 9 +- .../v1/modules/a76/country_rule_oct/models.py | 18 +- .../v1/modules/a76/exchange_rate/models.py | 9 +- .../a76/fraction_rule_octave/models.py | 8 +- backend/api/v1/modules/a76/package/models.py | 5 +- backend/api/v1/modules/a76/parts/dto.py | 8 +- backend/api/v1/modules/a76/parts/models.py | 20 +- backend/api/v1/modules/a76/parts/routes.py | 64 +- backend/api/v1/modules/a76/parts/service.py | 40 +- .../models/pedimento_config_additional.py | 28 +- .../models/pedimento_config_calculations.py | 36 +- .../models/pedimento_config_parameters.py | 38 +- .../models/pedimento_config_surcharges.py | 27 +- .../pedimento_config_update_rectification.py | 25 +- .../models/pedimento_config_updates.py | 23 +- .../models/pedimento_customs_offices.py | 19 +- .../a76/pedmientos/models/pedimento_dates.py | 41 +- .../models/pedimento_decrementables.py | 34 +- .../models/pedimento_incrementables.py | 36 +- .../pedmientos/models/pedimento_indexes.py | 22 +- .../pedmientos/models/pedimento_payments.py | 41 +- .../pedimento_rectification_destination.py | 31 +- .../models/pedimento_rectification_origin.py | 49 +- .../models/pedimento_transport_means.py | 26 +- .../pedmientos/models/pedimento_validation.py | 32 +- .../a76/pedmientos/models/pedimentos.py | 12 +- .../modules/a76/permission_rule_oct/models.py | 8 +- backend/api/v1/modules/a76/seal/models.py | 8 +- .../code_pedimento_regimens/models.py | 6 +- .../reference_data/pedimento_codes/models.py | 6 +- .../pedimento_regimens/models.py | 5 +- docs/MODULOS_A76_IMPLEMENTADOS.md | 28 +- docs/RELATIONSHIPS.md | 8 +- scripts/init_first_time.sh | 613 ++++++++++++++++++ 41 files changed, 1185 insertions(+), 418 deletions(-) create mode 100644 backend/api/v1/modules/a24/q/q_classes/models.py create mode 100644 backend/api/v1/modules/a24/s/s_classes/models.py create mode 100755 scripts/init_first_time.sh diff --git a/backend/api/v1/modules/a24/q/q_classes/models.py b/backend/api/v1/modules/a24/q/q_classes/models.py new file mode 100644 index 00000000..4a22b159 --- /dev/null +++ b/backend/api/v1/modules/a24/q/q_classes/models.py @@ -0,0 +1,32 @@ +from decimal import Decimal +from sqlalchemy import Boolean, ForeignKeyConstraint, Integer, Numeric, PrimaryKeyConstraint, String +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.orm.base import Mapped + +from core.database import Base + +class QClasses(Base): + __tablename__ = 'q_classes' #QClases + __table_args__ = ( + PrimaryKeyConstraint('id', name='qclases_pk'), + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_qclasses_tenants'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_qclasses_company'), + ForeignKeyConstraint(['class_id'], ['classes.id'], name='fk_qclasses_classes'), + {'schema': 'a24'} + ) + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + class_id: Mapped[int] = mapped_column(Integer, nullable=False) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=True, index=True) + + import_tariff_code: Mapped[str] = mapped_column(String(10)) #FRACCIONIMPO + import_tariff_type: Mapped[str] = mapped_column(String(6)) #TIPOFRACIMPO + export_tariff_code: Mapped[str] = mapped_column(String(10)) #FRACCIONEXPO + export_tariff_type: Mapped[str] = mapped_column(String(6)) #TIPOFRACEXPO + depreciation_rate: Mapped[Decimal] = mapped_column(Numeric(5, 2)) #TASADEPRECIA + fda_code: Mapped[str] = mapped_column(String(20)) #FDA + eccn_code: Mapped[str] = mapped_column(String(20)) #ECCN + class_enabled: Mapped[bool] = mapped_column(Boolean) #HABILITADESHABILITACLASE + + diff --git a/backend/api/v1/modules/a24/s/s_classes/models.py b/backend/api/v1/modules/a24/s/s_classes/models.py new file mode 100644 index 00000000..047a4e93 --- /dev/null +++ b/backend/api/v1/modules/a24/s/s_classes/models.py @@ -0,0 +1,21 @@ +from sqlalchemy import ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.orm.base import Mapped +from core.database import Base + +class SClasses(Base): + __tablename__ = 's_classes' #SClases + __table_args__ = ( + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_sclasses_tenants'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_sclasses_company'), + ForeignKeyConstraint(['class_id'], ['a76.clases.class_id'], name='fk_sclasses_classes'), + PrimaryKeyConstraint('id', name='sclases_pk'), + {'schema': 'a24'} + ) + id: Mapped[int] = mapped_column(Integer, primary_key=True) + class_id: Mapped[int] = mapped_column(Integer, nullable=False) #Id de clase + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=True, index=True) + + stock_um: Mapped[str] = mapped_column(String(5)) #Unidad de medida para existencia + us_tariff_code: Mapped[str] = mapped_column(String(19)) #Fracción americana (USA) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/classes/dto.py b/backend/api/v1/modules/a76/classes/dto.py index a0e9e786..7607c48e 100644 --- a/backend/api/v1/modules/a76/classes/dto.py +++ b/backend/api/v1/modules/a76/classes/dto.py @@ -9,7 +9,7 @@ from datetime import datetime class ClassCreateDTO(BaseModel): """DTO para crear una clase""" - client_key: int = Field(..., description="Client key") + client_id: int = Field(..., description="Client key") class_code: str = Field(..., max_length=8, description="Class code") description_spanish: Optional[str] = Field(None, max_length=500, description="Description in Spanish") description_english: Optional[str] = Field(None, max_length=500, description="Description in English") @@ -43,7 +43,7 @@ class ClassUpdateDTO(BaseModel): class ClassResponseDTO(BaseModel): """DTO para respuesta de clase""" - client_key: int + client_id: int class_code: str description_spanish: Optional[str] = None description_english: Optional[str] = None @@ -61,7 +61,7 @@ class ClassResponseDTO(BaseModel): class ClassBasicDTO(BaseModel): """DTO para información básica de clase""" - client_key: int + client_id: int class_code: str description_spanish: Optional[str] = None description_english: Optional[str] = None @@ -85,7 +85,7 @@ class ClassListDTO(BaseModel): class ClassSearchDTO(BaseModel): """DTO para búsqueda de clases""" - client_key: Optional[int] = Field(None, description="Filter by client key") + client_id: Optional[int] = Field(None, description="Filter by client key") class_code: Optional[str] = Field(None, description="Search by class code") description: Optional[str] = Field(None, description="Search in descriptions") material_key: Optional[str] = Field(None, description="Filter by material key") diff --git a/backend/api/v1/modules/a76/classes/models.py b/backend/api/v1/modules/a76/classes/models.py index 1257cf53..babb9994 100644 --- a/backend/api/v1/modules/a76/classes/models.py +++ b/backend/api/v1/modules/a76/classes/models.py @@ -18,30 +18,32 @@ class Class(Base): __tablename__ = "classes" __table_args__ = ( PrimaryKeyConstraint('id', name='classes_pkey'), - UniqueConstraint('client_key', 'class_code', name='uq_classes_client_key_class_code'), ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_classes_tenant'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_classes_company'), + ForeignKeyConstraint(['client_id'], ['a76.client_provider.id'], name='fk_classes_client'), ForeignKeyConstraint(['material_key'], ['public.material_types.key'], name='fk_classes_material_type'), + UniqueConstraint('tenant_id', 'company_id', 'class_code', name='uq_classes_client_id_class_code'), {"schema": "a76"} ) id: Mapped[int] = mapped_column(Integer, primary_key=True) - - # Unique constraint compuesta - client_key: Mapped[int] = mapped_column() - class_code: Mapped[str] = mapped_column(String(8)) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + client_id: Mapped[int] = mapped_column(Integer) + + # Unique constraint compuesta + class_code: Mapped[str] = mapped_column(String(8)) #CLASE # Basic information - description_es: Mapped[Optional[str]] = mapped_column(String(500)) - description_en: Mapped[Optional[str]] = mapped_column(String(500)) + description_es: Mapped[Optional[str]] = mapped_column(String(500)) #DESCRIPCIONE + description_en: Mapped[Optional[str]] = mapped_column(String(500)) #DESCRIPCIONI # Material and measurement material_key: Mapped[Optional[str]] = mapped_column(String(10), ForeignKey('public.material_types.key')) # CLAVEMAT - homologated from TIPOMAT/TIPOMATEQUIPO unit_of_measure: Mapped[Optional[str]] = mapped_column(String(5)) # UNIMED - homologated from UNIMEDIDA # Tariff fractions - fraction: Mapped[Optional[str]] = mapped_column(String(10)) # Mexican tariff fraction + fraction: Mapped[Optional[str]] = mapped_column(String(10)) # FRACCION us_fraction: Mapped[Optional[str]] = mapped_column(String(16)) # FRACCIONAME - US tariff fraction # Additional classification @@ -54,13 +56,13 @@ class Class(Base): # Inverse relationship with GParts that have this class parts: Mapped[list["Part"]] = relationship( - primaryjoin="and_(Class.client_key == Part.client_key, Class.class_code == Part.part_class)", - foreign_keys="[Part.client_key, Part.part_class]", + primaryjoin="and_(Class.client_id == Part.client_id, Class.class_code == Part.part_class)", + foreign_keys="[Part.client_id, Part.part_class]", viewonly=True, back_populates="part_class_info" ) def __repr__(self) -> str: - return f"" + return f"" diff --git a/backend/api/v1/modules/a76/classes/routes.py b/backend/api/v1/modules/a76/classes/routes.py index f0b906ab..e602a0e5 100644 --- a/backend/api/v1/modules/a76/classes/routes.py +++ b/backend/api/v1/modules/a76/classes/routes.py @@ -19,25 +19,11 @@ from .dto import ( router = APIRouter(prefix="/classes", tags=["Classes"]) - -@router.post("/", response_model=ClassResponseDTO, status_code=status.HTTP_201_CREATED) -async def create_class( - class_data: ClassCreateDTO, - db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) -): - """ - Create a new class in the system - """ - service = ClassService(db) - return service.create_class(class_data) - - @router.get("/", response_model=ClassListDTO) async def list_classes( skip: int = Query(0, ge=0, description="Number of records to skip"), limit: int = Query(100, ge=1, le=1000, description="Maximum number of records to return"), - client_key: Optional[int] = Query(None, description="Filter by client key"), + client_id: Optional[int] = Query(None, description="Filter by client key"), class_code: Optional[str] = Query(None, description="Search by class code"), description: Optional[str] = Query(None, description="Search in descriptions"), material_key: Optional[str] = Query(None, description="Filter by material key"), @@ -51,7 +37,7 @@ async def list_classes( """ service = ClassService(db) search_params = ClassSearchDTO( - client_key=client_key, + client_id=client_id, class_code=class_code, description=description, material_key=material_key, @@ -61,9 +47,9 @@ async def list_classes( return service.list_classes(skip, limit, search_params) -@router.get("/client/{client_key}", response_model=List[ClassBasicDTO]) +@router.get("/client/{client_id}", response_model=List[ClassBasicDTO]) async def get_classes_by_client( - client_key: int, + client_id: int, skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), db: Session = Depends(get_core_db), @@ -73,7 +59,7 @@ async def get_classes_by_client( Get all classes for a specific client """ service = ClassService(db) - return service.search_by_client(client_key, skip, limit) + return service.search_by_client(client_id, skip, limit) @router.get("/search/fraction/{fraction}", response_model=List[ClassBasicDTO]) @@ -140,29 +126,40 @@ async def get_classes_statistics( return service.get_classes_statistics() -@router.get("/{client_key}/{class_code}", response_model=ClassResponseDTO) +@router.get("/{client_id}/{class_code}", response_model=ClassResponseDTO) async def get_class( - client_key: int, + client_id: int, class_code: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) ): """ - Get class by composite key (client_key + class_code) + Get class by composite key (client_id + class_code) """ service = ClassService(db) - class_obj = service.get_class(client_key, class_code) + class_obj = service.get_class(client_id, class_code) if not class_obj: raise HTTPException( status_code=404, - detail=f"Class with client_key '{client_key}' and class_code '{class_code}' not found" + detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found" ) return class_obj +@router.post("/", response_model=ClassResponseDTO, status_code=status.HTTP_201_CREATED) +async def create_class( + class_data: ClassCreateDTO, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user) +): + """ + Create a new class in the system + """ + service = ClassService(db) + return service.create_class(class_data) -@router.put("/{client_key}/{class_code}", response_model=ClassResponseDTO) +@router.put("/{client_id}/{class_code}", response_model=ClassResponseDTO) async def update_class( - client_key: int, + client_id: int, class_code: str, class_data: ClassUpdateDTO, db: Session = Depends(get_core_db), @@ -172,18 +169,18 @@ async def update_class( Update class information """ service = ClassService(db) - class_obj = service.update_class(client_key, class_code, class_data) + class_obj = service.update_class(client_id, class_code, class_data) if not class_obj: raise HTTPException( status_code=404, - detail=f"Class with client_key '{client_key}' and class_code '{class_code}' not found" + detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found" ) return class_obj -@router.delete("/{client_key}/{class_code}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete("/{client_id}/{class_code}", status_code=status.HTTP_204_NO_CONTENT) async def delete_class( - client_key: int, + client_id: int, class_code: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) @@ -194,17 +191,17 @@ async def delete_class( Note: This will completely remove the class from the system. """ service = ClassService(db) - if not service.delete_class(client_key, class_code): + if not service.delete_class(client_id, class_code): raise HTTPException( status_code=404, - detail=f"Class with client_key '{client_key}' and class_code '{class_code}' not found" + detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found" ) # Endpoints específicos para información detallada -@router.get("/{client_key}/{class_code}/basic", response_model=ClassBasicDTO) +@router.get("/{client_id}/{class_code}/basic", response_model=ClassBasicDTO) async def get_class_basic_info( - client_key: int, + client_id: int, class_code: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) @@ -213,15 +210,15 @@ async def get_class_basic_info( Get basic information for a class """ service = ClassService(db) - class_obj = service.get_class(client_key, class_code) + class_obj = service.get_class(client_id, class_code) if not class_obj: raise HTTPException( status_code=404, - detail=f"Class with client_key '{client_key}' and class_code '{class_code}' not found" + detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found" ) return ClassBasicDTO( - client_key=class_obj.client_key, + client_id=class_obj.client_id, class_code=class_obj.class_code, description_spanish=class_obj.description_spanish, description_english=class_obj.description_english, @@ -230,9 +227,9 @@ async def get_class_basic_info( ) -@router.get("/{client_key}/{class_code}/tariff", response_model=dict) +@router.get("/{client_id}/{class_code}/tariff", response_model=dict) async def get_class_tariff_info( - client_key: int, + client_id: int, class_code: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) @@ -241,15 +238,15 @@ async def get_class_tariff_info( Get tariff information for a class (fractions, IVA exempt, etc.) """ service = ClassService(db) - class_obj = service.get_class(client_key, class_code) + class_obj = service.get_class(client_id, class_code) if not class_obj: raise HTTPException( status_code=404, - detail=f"Class with client_key '{client_key}' and class_code '{class_code}' not found" + detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found" ) return { - "client_key": class_obj.client_key, + "client_id": class_obj.client_id, "class_code": class_obj.class_code, "fraction": class_obj.fraction, "us_fraction": class_obj.us_fraction, diff --git a/backend/api/v1/modules/a76/classes/service.py b/backend/api/v1/modules/a76/classes/service.py index 6a11895b..b7ddf7d5 100644 --- a/backend/api/v1/modules/a76/classes/service.py +++ b/backend/api/v1/modules/a76/classes/service.py @@ -44,7 +44,7 @@ class ClassService: # Verificar que no exista la clase existing = self.db.query(Class).filter( and_( - Class.client_key == class_data.client_key, + Class.client_id == class_data.client_id, Class.class_code == class_data.class_code ) ).first() @@ -52,12 +52,12 @@ class ClassService: if existing: raise HTTPException( status_code=400, - detail=f"Class with client_key '{class_data.client_key}' and class_code '{class_data.class_code}' already exists" + detail=f"Class with client_id '{class_data.client_id}' and class_code '{class_data.class_code}' already exists" ) # Crear clase db_class = Class( - client_key=class_data.client_key, + client_id=class_data.client_id, class_code=class_data.class_code, description_spanish=class_data.description_spanish, description_english=class_data.description_english, @@ -74,14 +74,14 @@ class ClassService: self.db.commit() self.db.refresh(db_class) - logger.info(f"Class created: {db_class.client_key}-{db_class.class_code}") + logger.info(f"Class created: {db_class.client_id}-{db_class.class_code}") return ClassResponseDTO.model_validate(db_class) except IntegrityError as e: self.db.rollback() logger.error(f"IntegrityError creating class: {str(e)}") - raise HTTPException(status_code=400, detail="Class with this client_key and class_code already exists") + raise HTTPException(status_code=400, detail="Class with this client_id and class_code already exists") except HTTPException: raise except Exception as e: @@ -89,12 +89,12 @@ class ClassService: logger.error(f"Error creating class: {str(e)}") raise HTTPException(status_code=500, detail="Error creating class") - def get_class(self, client_key: int, class_code: str) -> Optional[ClassResponseDTO]: + def get_class(self, client_id: int, class_code: str) -> Optional[ClassResponseDTO]: """ Obtiene una clase por clave compuesta Args: - client_key: Clave del cliente + client_id: Clave del cliente class_code: Código de clase Returns: @@ -102,7 +102,7 @@ class ClassService: """ class_obj = self.db.query(Class).filter( and_( - Class.client_key == client_key, + Class.client_id == client_id, Class.class_code == class_code ) ).first() @@ -132,8 +132,8 @@ class ClassService: # Aplicar filtros si se proporcionan if search_params: - if search_params.client_key: - query = query.filter(Class.client_key == search_params.client_key) + if search_params.client_id: + query = query.filter(Class.client_id == search_params.client_id) if search_params.class_code: query = query.filter(Class.class_code.ilike(f"%{search_params.class_code}%")) @@ -172,12 +172,12 @@ class ClassService: size=len(class_dtos) ) - def update_class(self, client_key: int, class_code: str, class_data: ClassUpdateDTO) -> Optional[ClassResponseDTO]: + def update_class(self, client_id: int, class_code: str, class_data: ClassUpdateDTO) -> Optional[ClassResponseDTO]: """ Actualiza una clase Args: - client_key: Clave del cliente + client_id: Clave del cliente class_code: Código de clase class_data: Datos a actualizar @@ -186,7 +186,7 @@ class ClassService: """ class_obj = self.db.query(Class).filter( and_( - Class.client_key == client_key, + Class.client_id == client_id, Class.class_code == class_code ) ).first() @@ -202,21 +202,21 @@ class ClassService: self.db.commit() self.db.refresh(class_obj) - logger.info(f"Class updated: {client_key}-{class_code}") + logger.info(f"Class updated: {client_id}-{class_code}") return ClassResponseDTO.model_validate(class_obj) except Exception as e: self.db.rollback() - logger.error(f"Error updating class {client_key}-{class_code}: {str(e)}") + logger.error(f"Error updating class {client_id}-{class_code}: {str(e)}") raise HTTPException(status_code=500, detail="Error updating class") - def delete_class(self, client_key: int, class_code: str) -> bool: + def delete_class(self, client_id: int, class_code: str) -> bool: """ Elimina una clase Args: - client_key: Clave del cliente + client_id: Clave del cliente class_code: Código de clase Returns: @@ -224,7 +224,7 @@ class ClassService: """ class_obj = self.db.query(Class).filter( and_( - Class.client_key == client_key, + Class.client_id == client_id, Class.class_code == class_code ) ).first() @@ -235,11 +235,11 @@ class ClassService: try: self.db.delete(class_obj) self.db.commit() - logger.info(f"Class deleted: {client_key}-{class_code}") + logger.info(f"Class deleted: {client_id}-{class_code}") return True except Exception as e: self.db.rollback() - logger.error(f"Error deleting class {client_key}-{class_code}: {str(e)}") + logger.error(f"Error deleting class {client_id}-{class_code}: {str(e)}") raise HTTPException(status_code=500, detail="Error deleting class") def search_by_fraction(self, fraction: str) -> List[ClassBasicDTO]: @@ -247,9 +247,9 @@ class ClassService: classes = self.db.query(Class).filter(Class.fraction.ilike(f"%{fraction}%")).all() return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes] - def search_by_client(self, client_key: int, skip: int = 0, limit: int = 100) -> List[ClassBasicDTO]: + def search_by_client(self, client_id: int, skip: int = 0, limit: int = 100) -> List[ClassBasicDTO]: """Obtiene todas las clases de un cliente específico""" - classes = self.db.query(Class).filter(Class.client_key == client_key).offset(skip).limit(limit).all() + classes = self.db.query(Class).filter(Class.client_id == client_id).offset(skip).limit(limit).all() return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes] def search_by_material(self, material_key: str) -> List[ClassBasicDTO]: @@ -267,7 +267,7 @@ class ClassService: total_classes = self.db.query(Class).count() # Contar por clientes - clients_count = self.db.query(Class.client_key).distinct().count() + clients_count = self.db.query(Class.client_id).distinct().count() # Contar por revisión física physical_review_stats = {} diff --git a/backend/api/v1/modules/a76/client_and_provider/models.py b/backend/api/v1/modules/a76/client_and_provider/models.py index 0a1b366b..8c407369 100644 --- a/backend/api/v1/modules/a76/client_and_provider/models.py +++ b/backend/api/v1/modules/a76/client_and_provider/models.py @@ -16,11 +16,14 @@ class ClientProvider(Base): __table_args__ = ( PrimaryKeyConstraint('id', name='client_provider_pkey'), ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_client_provider_tenant'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_client_provider_company'), {"schema": "a76"} ) # Primary key id: Mapped[int] = mapped_column(Integer, primary_key=True) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) # Basic information type_nat_foreign: Mapped[Optional[str]] = mapped_column(String(1)) # TIPO NACIONAL/EXTRANJERO @@ -37,8 +40,7 @@ class ClientProvider(Base): position: Mapped[Optional[str]] = mapped_column(String(30)) incoterm: Mapped[Optional[str]] = mapped_column(String(19)) is_national_provider: Mapped[Optional[str]] = mapped_column(String(2)) - enabled_disabled: Mapped[Optional[int]] = mapped_column(SmallInteger) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + enabled_disabled: Mapped[Optional[int]] = mapped_column(SmallInteger) # Relationships address: Mapped[Optional["ClientProviderAddress"]] = relationship(back_populates="client_provider", uselist=False, cascade="all, delete-orphan") diff --git a/backend/api/v1/modules/a76/company/models.py b/backend/api/v1/modules/a76/company/models.py index 0ff67f22..8ab99331 100644 --- a/backend/api/v1/modules/a76/company/models.py +++ b/backend/api/v1/modules/a76/company/models.py @@ -3,7 +3,7 @@ Modelos ORM para gestión de empresa """ from typing import Optional from datetime import datetime -from sqlalchemy import DateTime, Integer, String, Boolean, SmallInteger, ForeignKey, PrimaryKeyConstraint, ForeignKeyConstraint +from sqlalchemy import DateTime, Integer, String, Boolean, SmallInteger, ForeignKey, PrimaryKeyConstraint, ForeignKeyConstraint, UniqueConstraint from sqlalchemy.sql import func from sqlalchemy.orm import Mapped, mapped_column from core.database import Base @@ -16,12 +16,13 @@ class Company(Base): __tablename__ = "company" __table_args__ = ( PrimaryKeyConstraint('id', name='company_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_company_tenant'), + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_company_tenant'), {"schema": "a76"} ) # Primary key id: Mapped[int] = mapped_column(Integer, primary_key=True) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) # Información básica de la empresa name: Mapped[Optional[str]] = mapped_column(String(255)) @@ -68,8 +69,6 @@ class Company(Base): # Timestamps created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) - deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) diff --git a/backend/api/v1/modules/a76/country_rule_oct/models.py b/backend/api/v1/modules/a76/country_rule_oct/models.py index b60ad7c5..38eb2b29 100644 --- a/backend/api/v1/modules/a76/country_rule_oct/models.py +++ b/backend/api/v1/modules/a76/country_rule_oct/models.py @@ -6,21 +6,25 @@ from core.database import Base class CountryRuleOct(Base): __tablename__ = "country_rule_oct" __table_args__ = ( - PrimaryKeyConstraint('id', name='country_rule_oct_pkey'), - UniqueConstraint('permission', 'line', 'fraction', 'country_code', name='uq_country_rule_oct_permission_line_fraction_country'), + PrimaryKeyConstraint('id', name='country_rule_oct_pkey'), + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_country_rule_oct_tenant'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_country_rule_oct_company'), ForeignKeyConstraint( - ['permission', 'line', 'fraction'], - ['a76.fraction_rule_octave.permission', 'a76.fraction_rule_octave.line', 'a76.fraction_rule_octave.fraction'], + ['tenant_id', 'company_id', 'permission', 'line', 'fraction'], + ['a76.fraction_rule_octave.tenant_id', 'a76.fraction_rule_octave.company_id', 'a76.fraction_rule_octave.permission', 'a76.fraction_rule_octave.line', 'a76.fraction_rule_octave.fraction'], ondelete="CASCADE", name='fk_country_rule_oct_frac_octava' - ), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_country_rule_oct_tenant'), + ), + UniqueConstraint('tenant_id', 'company_id', 'permission', 'line', 'fraction', 'country_code', name='uq_country_rule_oct_permission_line_fraction_country'), {"schema": "a76"} ) id: Mapped[int] = mapped_column(Integer, primary_key=True) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + permission: Mapped[str] = mapped_column(String(20)) line: Mapped[int] = mapped_column() fraction: Mapped[str] = mapped_column(String(10)) country_code: Mapped[str] = mapped_column(String(3)) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) \ No newline at end of file + \ No newline at end of file diff --git a/backend/api/v1/modules/a76/exchange_rate/models.py b/backend/api/v1/modules/a76/exchange_rate/models.py index 00d77d87..b8422fb4 100644 --- a/backend/api/v1/modules/a76/exchange_rate/models.py +++ b/backend/api/v1/modules/a76/exchange_rate/models.py @@ -10,13 +10,16 @@ class ExchangeRate(Base): __table_args__ = ( PrimaryKeyConstraint('id', name='exchange_rate_pkey'), ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_exchange_rate_tenant'), - UniqueConstraint('date', 'tenant_id', name='uq_exchange_rate_date_tenant'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_exchange_rate_company'), + UniqueConstraint('tenant_id', 'company_id', 'date', name='uq_exchange_rate_date_tenant'), {"schema": "a76"} ) id: Mapped[int] = mapped_column(Integer, primary_key=True) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + date: Mapped[int] = mapped_column(DateTime) value: Mapped[Optional[Decimal]] = mapped_column(DECIMAL(13, 6)) local_currency: Mapped[Optional[str]] = mapped_column(String(7)) - foreign_currency: Mapped[Optional[str]] = mapped_column(String(7)) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) \ No newline at end of file + foreign_currency: Mapped[Optional[str]] = mapped_column(String(7)) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/fraction_rule_octave/models.py b/backend/api/v1/modules/a76/fraction_rule_octave/models.py index c277c9b5..61d2ec2c 100644 --- a/backend/api/v1/modules/a76/fraction_rule_octave/models.py +++ b/backend/api/v1/modules/a76/fraction_rule_octave/models.py @@ -7,13 +7,17 @@ class FractionRuleOctave(Base): __tablename__ = "fraction_rule_octave" __table_args__ = ( PrimaryKeyConstraint('id', name='fraction_rule_octave_pkey'), - UniqueConstraint('permission', 'line', 'fraction', name='uq_fraction_rule_octave_permission_line_fraction'), ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_fraction_rule_octave_tenant'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_fraction_rule_octave_company'), + UniqueConstraint('tenant_id', 'company_id', 'permission', 'line', 'fraction', name='uq_fraction_rule_octave_permission_line_fraction'), {"schema": "a76"} ) id: Mapped[int] = mapped_column(Integer, primary_key=True) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + permission: Mapped[str] = mapped_column(String(20)) line: Mapped[int] = mapped_column(Integer) fraction: Mapped[str] = mapped_column(String(10)) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) \ No newline at end of file + \ No newline at end of file diff --git a/backend/api/v1/modules/a76/package/models.py b/backend/api/v1/modules/a76/package/models.py index 6d2b9fa7..89326dbc 100644 --- a/backend/api/v1/modules/a76/package/models.py +++ b/backend/api/v1/modules/a76/package/models.py @@ -11,13 +11,16 @@ class Package(Base): __tablename__ = "packages" # GBultos __table_args__ = ( PrimaryKeyConstraint('id', name='packages_pkey'), - UniqueConstraint('key', name='packages_key_ukey'), ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_packages_tenant'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_packages_company'), + UniqueConstraint('tenant_id', 'company_id', 'key', name='packages_key_ukey'), {"schema": "a76"} ) id: Mapped[int] = mapped_column(Integer, primary_key=True) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + key: Mapped[str] = mapped_column(String(5)) description_es: Mapped[Optional[str]] = mapped_column(String(40)) description_en: Mapped[Optional[str]] = mapped_column(String(40)) diff --git a/backend/api/v1/modules/a76/parts/dto.py b/backend/api/v1/modules/a76/parts/dto.py index 13524abd..00c703af 100644 --- a/backend/api/v1/modules/a76/parts/dto.py +++ b/backend/api/v1/modules/a76/parts/dto.py @@ -10,7 +10,7 @@ from decimal import Decimal class PartCreateDTO(BaseModel): """DTO para crear una parte""" - client_key: int = Field(..., description="Client key") + client_id: int = Field(..., description="Client key") part_number: str = Field(..., max_length=49, description="Part number") fraction: Optional[str] = Field(None, max_length=10, description="Tariff fraction") description_spanish: Optional[str] = Field(None, max_length=500, description="Description in Spanish") @@ -95,7 +95,7 @@ class PartUpdateDTO(BaseModel): class PartResponseDTO(BaseModel): """DTO para respuesta de parte""" - client_key: int + client_id: int part_number: str fraction: Optional[str] = None description_spanish: Optional[str] = None @@ -143,7 +143,7 @@ class PartResponseDTO(BaseModel): class PartBasicDTO(BaseModel): """DTO para información básica de parte""" - client_key: int + client_id: int part_number: str description_spanish: Optional[str] = None description_english: Optional[str] = None @@ -169,7 +169,7 @@ class PartListDTO(BaseModel): class PartSearchDTO(BaseModel): """DTO para búsqueda de partes""" - client_key: Optional[int] = Field(None, description="Filter by client key") + client_id: Optional[int] = Field(None, description="Filter by client key") part_number: Optional[str] = Field(None, description="Search by part number") description: Optional[str] = Field(None, description="Search in descriptions") fraction: Optional[str] = Field(None, description="Filter by tariff fraction") diff --git a/backend/api/v1/modules/a76/parts/models.py b/backend/api/v1/modules/a76/parts/models.py index 8d17a177..84c220ba 100644 --- a/backend/api/v1/modules/a76/parts/models.py +++ b/backend/api/v1/modules/a76/parts/models.py @@ -22,20 +22,20 @@ class Part(Base): __tablename__ = "parts" __table_args__ = ( PrimaryKeyConstraint('id', name='parts_pkey'), - UniqueConstraint('client_key', 'part_number', name='client_part_ukey'), - ForeignKeyConstraint(['country_of_origin'], ['public.countries.m3_key'], name='fk_parts_country'), - ForeignKeyConstraint(['currency_key'], ['public.currency_types.code'], name='fk_parts_currency'), ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_parts_tenant'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_parts_company'), + ForeignKeyConstraint(['country_of_origin'], ['public.countries.m3_key'], name='fk_parts_country'), + ForeignKeyConstraint(['currency_key'], ['public.currency_types.code'], name='fk_parts_currency'), + UniqueConstraint('tenant_id', 'company_id', 'part_number', name='client_part_ukey'), {"schema": "a76"} ) id: Mapped[int] = mapped_column(Integer, primary_key=True) - - # Tenant tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) # Unique constraint compuesta - client_key: Mapped[int] = mapped_column(Integer) + client_id: Mapped[int] = mapped_column(Integer) part_number: Mapped[str] = mapped_column(String(49)) # Basic information @@ -84,15 +84,15 @@ class Part(Base): currency: Mapped[Optional["CurrencyType"]] = relationship(foreign_keys=[currency_key]) # Relationship with Class through composite foreign key - # Note: This requires both client_key and part_class to match client_key and class_code in Class + # Note: This requires both client_id and part_class to match client_id and class_code in Class part_class_info: Mapped[Optional["Class"]] = relationship( - primaryjoin="and_(Part.client_key == Class.client_key, Part.part_class == Class.class_code)", - foreign_keys="[Part.client_key, Part.part_class]", + primaryjoin="and_(Part.client_id == Class.client_id, Part.part_class == Class.class_code)", + foreign_keys="[Part.client_id, Part.part_class]", viewonly=True, back_populates="parts" ) def __repr__(self) -> str: - return f"" + return f"" diff --git a/backend/api/v1/modules/a76/parts/routes.py b/backend/api/v1/modules/a76/parts/routes.py index 4757fbc5..3c4e2691 100644 --- a/backend/api/v1/modules/a76/parts/routes.py +++ b/backend/api/v1/modules/a76/parts/routes.py @@ -37,7 +37,7 @@ async def create_part( async def list_parts( skip: int = Query(0, ge=0, description="Number of records to skip"), limit: int = Query(100, ge=1, le=1000, description="Maximum number of records to return"), - client_key: Optional[int] = Query(None, description="Filter by client key"), + client_id: Optional[int] = Query(None, description="Filter by client key"), part_number: Optional[str] = Query(None, description="Search by part number"), description: Optional[str] = Query(None, description="Search in descriptions"), fraction: Optional[str] = Query(None, description="Filter by tariff fraction"), @@ -51,7 +51,7 @@ async def list_parts( """ service = PartService(db) search_params = PartSearchDTO( - client_key=client_key, + client_id=client_id, part_number=part_number, description=description, fraction=fraction, @@ -61,9 +61,9 @@ async def list_parts( return service.list_parts(skip, limit, search_params) -@router.get("/client/{client_key}", response_model=List[PartBasicDTO]) +@router.get("/client/{client_id}", response_model=List[PartBasicDTO]) async def get_parts_by_client( - client_key: int, + client_id: int, skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), db: Session = Depends(get_core_db), @@ -73,7 +73,7 @@ async def get_parts_by_client( Get all parts for a specific client """ service = PartService(db) - return service.search_by_client(client_key, skip, limit) + return service.search_by_client(client_id, skip, limit) @router.get("/search/fraction/{fraction}", response_model=List[PartBasicDTO]) @@ -127,29 +127,29 @@ async def get_parts_statistics( return service.get_parts_statistics() -@router.get("/{client_key}/{part_number}", response_model=PartResponseDTO) +@router.get("/{client_id}/{part_number}", response_model=PartResponseDTO) async def get_part( - client_key: int, + client_id: int, part_number: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) ): """ - Get part by composite key (client_key + part_number) + Get part by composite key (client_id + part_number) """ service = PartService(db) - part = service.get_part(client_key, part_number) + part = service.get_part(client_id, part_number) if not part: raise HTTPException( status_code=404, - detail=f"Part with client_key '{client_key}' and part_number '{part_number}' not found" + detail=f"Part with client_id '{client_id}' and part_number '{part_number}' not found" ) return part -@router.put("/{client_key}/{part_number}", response_model=PartResponseDTO) +@router.put("/{client_id}/{part_number}", response_model=PartResponseDTO) async def update_part( - client_key: int, + client_id: int, part_number: str, part_data: PartUpdateDTO, db: Session = Depends(get_core_db), @@ -159,18 +159,18 @@ async def update_part( Update part information """ service = PartService(db) - part = service.update_part(client_key, part_number, part_data) + part = service.update_part(client_id, part_number, part_data) if not part: raise HTTPException( status_code=404, - detail=f"Part with client_key '{client_key}' and part_number '{part_number}' not found" + detail=f"Part with client_id '{client_id}' and part_number '{part_number}' not found" ) return part -@router.delete("/{client_key}/{part_number}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete("/{client_id}/{part_number}", status_code=status.HTTP_204_NO_CONTENT) async def delete_part( - client_key: int, + client_id: int, part_number: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) @@ -181,16 +181,16 @@ async def delete_part( Note: This will completely remove the part from the system. """ service = PartService(db) - if not service.delete_part(client_key, part_number): + if not service.delete_part(client_id, part_number): raise HTTPException( status_code=404, - detail=f"Part with client_key '{client_key}' and part_number '{part_number}' not found" + detail=f"Part with client_id '{client_id}' and part_number '{part_number}' not found" ) -@router.patch("/{client_key}/{part_number}/toggle-status", response_model=PartResponseDTO) +@router.patch("/{client_id}/{part_number}/toggle-status", response_model=PartResponseDTO) async def toggle_part_status( - client_key: int, + client_id: int, part_number: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) @@ -199,19 +199,19 @@ async def toggle_part_status( Toggle part enabled/disabled status """ service = PartService(db) - part = service.toggle_status(client_key, part_number) + part = service.toggle_status(client_id, part_number) if not part: raise HTTPException( status_code=404, - detail=f"Part with client_key '{client_key}' and part_number '{part_number}' not found" + detail=f"Part with client_id '{client_id}' and part_number '{part_number}' not found" ) return part # Endpoints específicos para información detallada -@router.get("/{client_key}/{part_number}/basic", response_model=PartBasicDTO) +@router.get("/{client_id}/{part_number}/basic", response_model=PartBasicDTO) async def get_part_basic_info( - client_key: int, + client_id: int, part_number: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) @@ -220,15 +220,15 @@ async def get_part_basic_info( Get basic information for a part """ service = PartService(db) - part = service.get_part(client_key, part_number) + part = service.get_part(client_id, part_number) if not part: raise HTTPException( status_code=404, - detail=f"Part with client_key '{client_key}' and part_number '{part_number}' not found" + detail=f"Part with client_id '{client_id}' and part_number '{part_number}' not found" ) return PartBasicDTO( - client_key=part.client_key, + client_id=part.client_id, part_number=part.part_number, description_spanish=part.description_spanish, description_english=part.description_english, @@ -239,9 +239,9 @@ async def get_part_basic_info( ) -@router.get("/{client_key}/{part_number}/regulatory", response_model=dict) +@router.get("/{client_id}/{part_number}/regulatory", response_model=dict) async def get_part_regulatory_info( - client_key: int, + client_id: int, part_number: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) @@ -250,15 +250,15 @@ async def get_part_regulatory_info( Get regulatory information for a part (FDA, FCC, ECCN, etc.) """ service = PartService(db) - part = service.get_part(client_key, part_number) + part = service.get_part(client_id, part_number) if not part: raise HTTPException( status_code=404, - detail=f"Part with client_key '{client_key}' and part_number '{part_number}' not found" + detail=f"Part with client_id '{client_id}' and part_number '{part_number}' not found" ) return { - "client_key": part.client_key, + "client_id": part.client_id, "part_number": part.part_number, "fraction": part.fraction, "us_fraction": part.us_fraction, diff --git a/backend/api/v1/modules/a76/parts/service.py b/backend/api/v1/modules/a76/parts/service.py index 11cbb13f..31c8c457 100644 --- a/backend/api/v1/modules/a76/parts/service.py +++ b/backend/api/v1/modules/a76/parts/service.py @@ -34,21 +34,21 @@ class PartService: except IntegrityError as e: db.rollback() logger.error(f"Error creating part: {e}") - raise HTTPException(status_code=400, detail="Part with this client_key and part_number already exists") + raise HTTPException(status_code=400, detail="Part with this client_id and part_number already exists") except Exception as e: db.rollback() logger.error(f"Unexpected error creating part: {e}") raise HTTPException(status_code=500, detail="Error creating part") @staticmethod - def get_part(db: Session, client_key: int, part_number: str) -> Optional[Part]: + def get_part(db: Session, client_id: int, part_number: str) -> Optional[Part]: """ Obtener una parte por clave de cliente y número de parte """ try: return db.query(Part).filter( and_( - Part.client_key == client_key, + Part.client_id == client_id, Part.part_number == part_number ) ).first() @@ -62,7 +62,7 @@ class PartService: skip: int = 0, limit: int = 100, search: Optional[str] = None, - client_key: Optional[int] = None, + client_id: Optional[int] = None, fraction: Optional[str] = None, country_of_origin: Optional[str] = None ) -> tuple[List[Part], int]: @@ -80,8 +80,8 @@ class PartService: Part.part_number.ilike(f"%{search}%") )) - if client_key is not None: - query = query.filter(Part.client_key == client_key) + if client_id is not None: + query = query.filter(Part.client_id == client_id) if fraction: query = query.filter(Part.fraction == fraction) @@ -101,12 +101,12 @@ class PartService: raise HTTPException(status_code=500, detail="Error retrieving parts") @staticmethod - def get_parts_by_client(db: Session, client_key: int) -> List[Part]: + def get_parts_by_client(db: Session, client_id: int) -> List[Part]: """ Obtener todas las partes de un cliente específico """ try: - return db.query(Part).filter(Part.client_key == client_key).all() + return db.query(Part).filter(Part.client_id == client_id).all() except Exception as e: logger.error(f"Error getting parts by client: {e}") raise HTTPException(status_code=500, detail="Error retrieving client parts") @@ -150,12 +150,12 @@ class PartService: raise HTTPException(status_code=500, detail="Error searching parts by country") @staticmethod - def update_part(db: Session, client_key: int, part_number: str, part_data: PartUpdateDTO) -> Optional[Part]: + def update_part(db: Session, client_id: int, part_number: str, part_data: PartUpdateDTO) -> Optional[Part]: """ Actualizar una parte existente """ try: - db_part = PartService.get_part(db, client_key, part_number) + db_part = PartService.get_part(db, client_id, part_number) if not db_part: return None @@ -172,12 +172,12 @@ class PartService: raise HTTPException(status_code=500, detail="Error updating part") @staticmethod - def delete_part(db: Session, client_key: int, part_number: str) -> bool: + def delete_part(db: Session, client_id: int, part_number: str) -> bool: """ Eliminar una parte """ try: - db_part = PartService.get_part(db, client_key, part_number) + db_part = PartService.get_part(db, client_id, part_number) if not db_part: return False @@ -190,12 +190,12 @@ class PartService: raise HTTPException(status_code=500, detail="Error deleting part") @staticmethod - def toggle_part_status(db: Session, client_key: int, part_number: str) -> Optional[Part]: + def toggle_part_status(db: Session, client_id: int, part_number: str) -> Optional[Part]: """ Cambiar el estado habilitado/deshabilitado de una parte """ try: - db_part = PartService.get_part(db, client_key, part_number) + db_part = PartService.get_part(db, client_id, part_number) if not db_part: return None @@ -220,9 +220,9 @@ class PartService: # Partes por cliente parts_by_client = db.query( - Part.client_key, + Part.client_id, func.count(Part.part_number).label('count') - ).group_by(Part.client_key).all() + ).group_by(Part.client_id).all() # Partes por país de origen parts_by_country = db.query( @@ -239,7 +239,7 @@ class PartService: "total_parts": total_parts, "enabled_parts": enabled_parts, "disabled_parts": disabled_parts, - "parts_by_client": [{"client_key": item[0], "count": item[1]} for item in parts_by_client], + "parts_by_client": [{"client_id": item[0], "count": item[1]} for item in parts_by_client], "parts_by_country": [{"country": item[0], "count": item[1]} for item in parts_by_country] } except Exception as e: @@ -247,17 +247,17 @@ class PartService: raise HTTPException(status_code=500, detail="Error retrieving parts statistics") @staticmethod - def get_part_regulatory_info(db: Session, client_key: int, part_number: str) -> Optional[dict]: + def get_part_regulatory_info(db: Session, client_id: int, part_number: str) -> Optional[dict]: """ Obtener información regulatoria específica de una parte """ try: - db_part = PartService.get_part(db, client_key, part_number) + db_part = PartService.get_part(db, client_id, part_number) if not db_part: return None return { - "client_key": db_part.client_key, + "client_id": db_part.client_id, "part_number": db_part.part_number, "fraction": db_part.fraction, "us_fraction": db_part.us_fraction, diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_additional.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_additional.py index 67f4ad96..e7d5e3b5 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_additional.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_additional.py @@ -1,7 +1,6 @@ from typing import TYPE_CHECKING from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, String, UniqueConstraint, func, text from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.orm.base import Mapped from datetime import datetime from core.database import Base @@ -12,22 +11,25 @@ if TYPE_CHECKING: class PedimentoConfigAdditional(Base): __tablename__ = 'pedimento_config_additional' __table_args__ = ( - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_config_additional'), PrimaryKeyConstraint('id', name='pedimento_config_additional_pkey'), - UniqueConstraint('pedimento_id', name='pedimento_config_additional_pedimento_id_key'), + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_config_additional_tenant'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_config_additional_company'), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_config_additional'), + UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_config_additional_pedimento_id_key'), {'schema': 'a76'} ) - id = mapped_column(Integer) - pedimento_id = mapped_column(Integer, nullable=False) - tenant_id = mapped_column(Integer, nullable=False, index=True) - add_po_identifier = mapped_column(SmallInteger) - do_not_exempt_norms_complement_x = mapped_column(SmallInteger) - manual_pedimento_year = mapped_column(String(2)) - enable_import_invoice_recipient = mapped_column(SmallInteger) - send_502_validation_file_for_consolidated = mapped_column(SmallInteger) - add_remove_norms = mapped_column(SmallInteger) + id: Mapped [int] = mapped_column(Integer) + tenant_id: Mapped [int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + pedimento_id: Mapped [int] = mapped_column(Integer, nullable=False) + + add_po_identifier: Mapped [int] = mapped_column(SmallInteger) + do_not_exempt_norms_complement_x: Mapped [int] = mapped_column(SmallInteger) + manual_pedimento_year: Mapped [str] = mapped_column(String(2)) + enable_import_invoice_recipient: Mapped [int] = mapped_column(SmallInteger) + send_502_validation_file_for_consolidated: Mapped [int] = mapped_column(SmallInteger) + add_remove_norms: Mapped [int] = mapped_column(SmallInteger) # Timestamps created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_calculations.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_calculations.py index d4b29047..e655fd4d 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_calculations.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_calculations.py @@ -1,7 +1,6 @@ from typing import TYPE_CHECKING from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, String, UniqueConstraint, func, text from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.orm.base import Mapped from datetime import datetime from core.database import Base @@ -11,26 +10,29 @@ if TYPE_CHECKING: class PedimentoConfigCalculations(Base): __tablename__ = 'pedimento_config_calculations' __table_args__ = ( - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_config_calculations'), PrimaryKeyConstraint('id', name='pedimento_config_calculations_pkey'), - UniqueConstraint('pedimento_id', name='pedimento_config_calculations_pedimento_id_key'), + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), + ForeignKeyConstraint(['company_id'], ['a76.company.id']), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_config_calculations'), + UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_config_calculations_pedimento_id_key'), {'schema': 'a76'} ) - id = mapped_column(Integer) - pedimento_id = mapped_column(Integer, nullable=False) - tenant_id = mapped_column(Integer, nullable=False, index=True) - dta_type = mapped_column(String(1)) - dta_operation = mapped_column(SmallInteger) - dta_vehicle_count = mapped_column(SmallInteger) - dta_mixed_rate_8permil = mapped_column(SmallInteger) - pays_vat = mapped_column(SmallInteger) - pays_prevalidation = mapped_column(SmallInteger) - include_sagar_certificate_fee = mapped_column(SmallInteger) - fixed_vehicle_dta_fee = mapped_column(SmallInteger) - additional_fixed_fee = mapped_column(SmallInteger) - additional_fixed_fee_payment_method = mapped_column(SmallInteger) + id: Mapped[int] = mapped_column(Integer) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + + dta_type: Mapped[str] = mapped_column(String(1)) + dta_operation: Mapped[int] = mapped_column(SmallInteger) + dta_vehicle_count: Mapped[int] = mapped_column(SmallInteger) + dta_mixed_rate_8permil: Mapped[int] = mapped_column(SmallInteger) + pays_vat: Mapped[int] = mapped_column(SmallInteger) + pays_prevalidation: Mapped[int] = mapped_column(SmallInteger) + include_sagar_certificate_fee: Mapped[int] = mapped_column(SmallInteger) + fixed_vehicle_dta_fee: Mapped[int] = mapped_column(SmallInteger) + additional_fixed_fee: Mapped[int] = mapped_column(SmallInteger) + additional_fixed_fee_payment_method: Mapped[int] = mapped_column(SmallInteger) # Timestamps created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_parameters.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_parameters.py index a30e59ff..fc8046c4 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_parameters.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_parameters.py @@ -1,3 +1,4 @@ +from decimal import Decimal from typing import TYPE_CHECKING from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, Numeric, PrimaryKeyConstraint, SmallInteger, UniqueConstraint, func, text from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -12,27 +13,30 @@ if TYPE_CHECKING: class PedimentoConfigParameters(Base): __tablename__ = 'pedimento_config_parameters' __table_args__ = ( - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_config_parameters'), PrimaryKeyConstraint('id', name='pedimento_config_parameters_pkey'), - UniqueConstraint('pedimento_id', name='pedimento_config_parameters_pedimento_id_key'), + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_config_parameters_tenant'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_config_parameters_company'), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_config_parameters'), + UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_config_parameters_pedimento_id_key'), {'schema': 'a76'} ) - id = mapped_column(Integer) - pedimento_id = mapped_column(Integer, nullable=False) - tenant_id = mapped_column(Integer, nullable=False, index=True) - is_embassy = mapped_column(SmallInteger) - embassy_dta = mapped_column(Numeric(11, 2)) - rule_3121_section_ii = mapped_column(SmallInteger) - use_previous_tariff = mapped_column(SmallInteger) - use_payment_date_fi = mapped_column(SmallInteger) - add_state_supplier_record_505 = mapped_column(SmallInteger) - customs_value_calculation = mapped_column(SmallInteger) - two_decimals_unit_value = mapped_column(SmallInteger) - customs_value_per_item = mapped_column(SmallInteger) - is_national_supplier = mapped_column(SmallInteger) - is_consolidated = mapped_column(SmallInteger) + id: Mapped[int] = mapped_column(Integer) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + + is_embassy: Mapped[int] = mapped_column(SmallInteger) + embassy_dta: Mapped[Decimal] = mapped_column(Numeric(11, 2)) + rule_3121_section_ii: Mapped[int] = mapped_column(SmallInteger) + use_previous_tariff: Mapped[int] = mapped_column(SmallInteger) + use_payment_date_fi: Mapped[int] = mapped_column(SmallInteger) + add_state_supplier_record_505: Mapped[int] = mapped_column(SmallInteger) + customs_value_calculation: Mapped[int] = mapped_column(SmallInteger) + two_decimals_unit_value: Mapped[int] = mapped_column(SmallInteger) + customs_value_per_item: Mapped[int] = mapped_column(SmallInteger) + is_national_supplier: Mapped[int] = mapped_column(SmallInteger) + is_consolidated: Mapped[int] = mapped_column(SmallInteger) # Timestamps created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_surcharges.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_surcharges.py index e36ef3d1..845211c2 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_surcharges.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_surcharges.py @@ -11,22 +11,25 @@ if TYPE_CHECKING: class PedimentoConfigSurcharges(Base): __tablename__ = 'pedimento_config_surcharges' __table_args__ = ( - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_config_surcharges'), PrimaryKeyConstraint('id', name='pedimento_config_surcharges_pkey'), - UniqueConstraint('pedimento_id', name='pedimento_config_surcharges_pedimento_id_key'), + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_config_surcharges_tenant'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_config_surcharges_company'), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_config_surcharges'), + UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_config_surcharges_pedimento_id_key'), {'schema': 'a76'} ) - id = mapped_column(Integer) - pedimento_id = mapped_column(Integer, nullable=False) - tenant_id = mapped_column(Integer, nullable=False, index=True) - surcharge_igi = mapped_column(SmallInteger) - surcharge_dta = mapped_column(SmallInteger) - surcharge_vat = mapped_column(SmallInteger) - surcharge_isan = mapped_column(SmallInteger) - surcharge_ieps = mapped_column(SmallInteger) - surcharge_cc = mapped_column(SmallInteger) + id: Mapped[int] = mapped_column(Integer) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + + surcharge_igi: Mapped[int] = mapped_column(SmallInteger) + surcharge_dta: Mapped[int] = mapped_column(SmallInteger) + surcharge_vat: Mapped[int] = mapped_column(SmallInteger) + surcharge_isan: Mapped[int] = mapped_column(SmallInteger) + surcharge_ieps: Mapped[int] = mapped_column(SmallInteger) + surcharge_cc: Mapped[int] = mapped_column(SmallInteger) # Timestamps created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_update_rectification.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_update_rectification.py index 29af4636..b32e759d 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_update_rectification.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_update_rectification.py @@ -11,21 +11,24 @@ if TYPE_CHECKING: class PedimentoConfigUpdateRectification(Base): __tablename__ = 'pedimento_config_update_rectification' __table_args__ = ( - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_config_update_rectification'), PrimaryKeyConstraint('id', name='pedimento_config_update_rectification_pkey'), - UniqueConstraint('pedimento_id', name='pedimento_config_update_rectification_pedimento_id_key'), + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_config_update_rectification_tenant'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_config_update_rectification_company'), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_config_update_rectification'), + UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_config_update_rectification_pedimento_id_key'), {'schema': 'a76'} ) - id = mapped_column(Integer) - pedimento_id = mapped_column(Integer, nullable=False) - tenant_id = mapped_column(Integer, nullable=False, index=True) - update_vat = mapped_column(SmallInteger) - update_advalorem = mapped_column(SmallInteger) - update_cc = mapped_column(SmallInteger) - update_ieps = mapped_column(SmallInteger) - calculate_surcharge = mapped_column(SmallInteger) + id: Mapped[int] = mapped_column(Integer) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + + update_vat: Mapped[int] = mapped_column(SmallInteger) + update_advalorem: Mapped[int] = mapped_column(SmallInteger) + update_cc: Mapped[int] = mapped_column(SmallInteger) + update_ieps: Mapped[int] = mapped_column(SmallInteger) + calculate_surcharge: Mapped[int] = mapped_column(SmallInteger) # Timestamps created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_updates.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_updates.py index 7162fb1d..a1f93a67 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_updates.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_updates.py @@ -11,20 +11,23 @@ if TYPE_CHECKING: class PedimentoConfigUpdates(Base): __tablename__ = 'pedimento_config_updates' __table_args__ = ( - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_config_updates'), PrimaryKeyConstraint('id', name='pedimento_config_updates_pkey'), - UniqueConstraint('pedimento_id', name='pedimento_config_updates_pedimento_id_key'), + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_config_updates_tenant'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_config_updates_company'), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_config_updates'), + UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_config_updates_pedimento_id_key'), {'schema': 'a76'} ) - id = mapped_column(Integer) - pedimento_id = mapped_column(Integer, nullable=False) - tenant_id = mapped_column(Integer, nullable=False, index=True) - update_vat = mapped_column(SmallInteger) - update_advalorem = mapped_column(SmallInteger) - update_cc = mapped_column(SmallInteger) - update_ieps = mapped_column(SmallInteger) + id: Mapped[int] = mapped_column(Integer) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + + update_vat: Mapped[int] = mapped_column(SmallInteger) + update_advalorem: Mapped[int] = mapped_column(SmallInteger) + update_cc: Mapped[int] = mapped_column(SmallInteger) + update_ieps: Mapped[int] = mapped_column(SmallInteger) # Timestamps created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_customs_offices.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_customs_offices.py index 5efc6fa0..beb1dc1e 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_customs_offices.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_customs_offices.py @@ -11,18 +11,21 @@ if TYPE_CHECKING: class PedimentoCustomsOffices(Base): __tablename__ = 'pedimento_customs_offices' __table_args__ = ( - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_customs_offices'), PrimaryKeyConstraint('id', name='pedimento_customs_offices_pkey'), - UniqueConstraint('pedimento_id', name='pedimento_customs_offices_pedimento_id_key'), + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_customs_offices_tenant'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_customs_offices_company'), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_customs_offices'), + UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_customs_offices_pedimento_id_key'), {'schema': 'a76'} ) - id = mapped_column(Integer) - pedimento_id = mapped_column(Integer, nullable=False) - tenant_id = mapped_column(Integer, nullable=False, index=True) - dispatch_customs = mapped_column(String(3)) - entry_exit_customs = mapped_column(String(3)) + id: Mapped[int] = mapped_column(Integer) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + + dispatch_customs: Mapped[str] = mapped_column(String(3)) + entry_exit_customs: Mapped[str] = mapped_column(String(3)) # Timestamps created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_dates.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_dates.py index 6a4141e0..61e87d3c 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_dates.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_dates.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING from sqlalchemy import DateTime, ForeignKeyConstraint, Index, Integer, PrimaryKeyConstraint, Time, UniqueConstraint, func, text from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm.base import Mapped -from datetime import datetime +from datetime import datetime, time as datetime_time from core.database import Base if TYPE_CHECKING: @@ -11,29 +11,32 @@ if TYPE_CHECKING: class PedimentoDates(Base): __tablename__ = 'pedimento_dates' __table_args__ = ( - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_dates'), PrimaryKeyConstraint('id', name='pedimento_dates_pkey'), - UniqueConstraint('pedimento_id', name='pedimento_dates_pedimento_id_key'), + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_dates_tenant'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_dates_company'), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_dates'), + UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_dates_pedimento_id_key'), Index('idx_pedimento_dates_pedimento_id', 'pedimento_id'), {'schema': 'a76'} ) - id = mapped_column(Integer) - pedimento_id = mapped_column(Integer, nullable=False) - tenant_id = mapped_column(Integer, nullable=False, index=True) - entry_date = mapped_column(DateTime) - pedimento_date = mapped_column(DateTime) - payment_date = mapped_column(DateTime) - rectification_payment_date = mapped_column(DateTime) - extraction_date = mapped_column(DateTime) - submission_date = mapped_column(DateTime) - eucan_date = mapped_column(DateTime) - original_date = mapped_column(DateTime) - start_date = mapped_column(DateTime) - end_date = mapped_column(DateTime) - capture_date = mapped_column(DateTime) - capture_time = mapped_column(Time) + id: Mapped[int] = mapped_column(Integer) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + + entry_date: Mapped[datetime] = mapped_column(DateTime) + pedimento_date: Mapped[datetime] = mapped_column(DateTime) + payment_date: Mapped[datetime] = mapped_column(DateTime) + rectification_payment_date: Mapped[datetime] = mapped_column(DateTime) + extraction_date: Mapped[datetime] = mapped_column(DateTime) + submission_date: Mapped[datetime] = mapped_column(DateTime) + eucan_date: Mapped[datetime] = mapped_column(DateTime) + original_date: Mapped[datetime] = mapped_column(DateTime) + start_date: Mapped[datetime] = mapped_column(DateTime) + end_date: Mapped[datetime] = mapped_column(DateTime) + capture_date: Mapped[datetime] = mapped_column(DateTime) + capture_time: Mapped[datetime_time] = mapped_column(Time) # Timestamps created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_decrementables.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_decrementables.py index 1b55c1d4..69aa462a 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_decrementables.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_decrementables.py @@ -1,3 +1,4 @@ +from decimal import Decimal from typing import TYPE_CHECKING from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, Numeric, PrimaryKeyConstraint, SmallInteger, String, UniqueConstraint, func, text from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -11,25 +12,28 @@ if TYPE_CHECKING: class PedimentoDecrementables(Base): __tablename__ = 'pedimento_decrementables' __table_args__ = ( - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_decrementables'), PrimaryKeyConstraint('id', name='pedimento_decrementables_pkey'), - UniqueConstraint('pedimento_id', name='pedimento_decrementables_pedimento_id_key'), + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_decrementables_tenant'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_decrementables_company'), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_decrementables'), + UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_decrementables_pedimento_id_key'), {'schema': 'a76'} ) - id = mapped_column(Integer) - pedimento_id = mapped_column(Integer, nullable=False) - tenant_id = mapped_column(Integer, nullable=False, index=True) - freight = mapped_column(Numeric(13, 2)) - insurance = mapped_column(Numeric(13, 2)) - loading = mapped_column(Numeric(13, 2)) - unloading = mapped_column(Numeric(13, 2)) - others = mapped_column(Numeric(13, 2)) - currency = mapped_column(String(3)) - currency_factor = mapped_column(Numeric(15, 8)) - not_affect_usd_value = mapped_column(SmallInteger) - not_affect_customs_value = mapped_column(SmallInteger) + id: Mapped[int] = mapped_column(Integer) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + + freight: Mapped[Decimal] = mapped_column(Numeric(13, 2)) + insurance: Mapped[Decimal] = mapped_column(Numeric(13, 2)) + loading: Mapped[Decimal] = mapped_column(Numeric(13, 2)) + unloading: Mapped[Decimal] = mapped_column(Numeric(13, 2)) + others: Mapped[Decimal] = mapped_column(Numeric(13, 2)) + currency: Mapped[str] = mapped_column(String(3)) + currency_factor: Mapped[Decimal] = mapped_column(Numeric(15, 8)) + not_affect_usd_value: Mapped[int] = mapped_column(SmallInteger) + not_affect_customs_value: Mapped[int] = mapped_column(SmallInteger) # Timestamps created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_incrementables.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_incrementables.py index 5889246d..b9023aa8 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_incrementables.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_incrementables.py @@ -1,3 +1,4 @@ +from decimal import Decimal from typing import TYPE_CHECKING from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, Numeric, PrimaryKeyConstraint, SmallInteger, String, UniqueConstraint, func, text from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -11,26 +12,29 @@ if TYPE_CHECKING: class PedimentoIncrementables(Base): __tablename__ = 'pedimento_incrementables' __table_args__ = ( - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_incrementables'), PrimaryKeyConstraint('id', name='pedimento_incrementables_pkey'), - UniqueConstraint('pedimento_id', name='pedimento_incrementables_pedimento_id_key'), + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_incrementables_tenant'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_incrementables_company'), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_incrementables'), + UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_incrementables_pedimento_id_key'), {'schema': 'a76'} ) - id = mapped_column(Integer) - pedimento_id = mapped_column(Integer, nullable=False) - tenant_id = mapped_column(Integer, nullable=False, index=True) - insured_value = mapped_column(Numeric(13, 2)) - freight = mapped_column(Numeric(13, 2)) - insurance = mapped_column(Numeric(13, 2)) - packaging = mapped_column(Numeric(13, 2)) - others = mapped_column(Numeric(13, 3)) - deductibles = mapped_column(Numeric(13, 3)) - currency = mapped_column(String(3)) - currency_factor = mapped_column(Numeric(15, 8)) - not_affect_usd_value = mapped_column(SmallInteger) - not_affect_customs_value = mapped_column(SmallInteger) + id: Mapped[int] = mapped_column(Integer) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + + insured_value: Mapped[Decimal] = mapped_column(Numeric(13, 2)) + freight: Mapped[Decimal] = mapped_column(Numeric(13, 2)) + insurance: Mapped[Decimal] = mapped_column(Numeric(13, 2)) + packaging: Mapped[Decimal] = mapped_column(Numeric(13, 2)) + others: Mapped[Decimal] = mapped_column(Numeric(13, 3)) + deductibles: Mapped[Decimal] = mapped_column(Numeric(13, 3)) + currency: Mapped[str] = mapped_column(String(3)) + currency_factor: Mapped[Decimal] = mapped_column(Numeric(15, 8)) + not_affect_usd_value: Mapped[int] = mapped_column(SmallInteger) + not_affect_customs_value: Mapped[int] = mapped_column(SmallInteger) # Timestamps created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_indexes.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_indexes.py index 0420b4a1..66d8478a 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_indexes.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_indexes.py @@ -1,3 +1,4 @@ +from decimal import Decimal from typing import TYPE_CHECKING from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, Numeric, PrimaryKeyConstraint, SmallInteger, UniqueConstraint, func, text from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -11,19 +12,22 @@ if TYPE_CHECKING: class PedimentoIndexes(Base): __tablename__ = 'pedimento_indexes' __table_args__ = ( - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_indexes'), PrimaryKeyConstraint('id', name='pedimento_indexes_pkey'), - UniqueConstraint('pedimento_id', name='pedimento_indexes_pedimento_id_key'), + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_indexes_tenant'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_indexes_company'), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_indexes'), + UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_indexes_pedimento_id_key'), {'schema': 'a76'} ) - id = mapped_column(Integer) - pedimento_id = mapped_column(Integer, nullable=False) - tenant_id = mapped_column(Integer, nullable=False, index=True) - update_factor_type = mapped_column(SmallInteger) - update_factor = mapped_column(Numeric(7, 4)) - manual_update_factor = mapped_column(SmallInteger) + id: Mapped[int] = mapped_column(Integer) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + + update_factor_type: Mapped[int] = mapped_column(SmallInteger) + update_factor: Mapped[Decimal] = mapped_column(Numeric(7, 4)) + manual_update_factor: Mapped[int] = mapped_column(SmallInteger) # Timestamps created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_payments.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_payments.py index f3a72e3a..b7fdfd62 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_payments.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_payments.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING from sqlalchemy import Date, DateTime, ForeignKeyConstraint, Index, Integer, PrimaryKeyConstraint, SmallInteger, String, Time, UniqueConstraint, func, text from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm.base import Mapped -from datetime import datetime +from datetime import datetime, time as Time2, date as Date2 from core.database import Base if TYPE_CHECKING: @@ -11,29 +11,32 @@ if TYPE_CHECKING: class PedimentoPayments(Base): __tablename__ = 'pedimento_payments' __table_args__ = ( - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_payments'), PrimaryKeyConstraint('id', name='pedimento_payments_pkey'), - UniqueConstraint('pedimento_id', name='pedimento_payments_pedimento_id_key'), + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_payments_tenant'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_payments_company'), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_payments'), + UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_payments_pedimento_id_key'), Index('idx_pedimento_payments_pedimento_id', 'pedimento_id'), {'schema': 'a76'} ) - id = mapped_column(Integer) - pedimento_id = mapped_column(Integer, nullable=False) - tenant_id = mapped_column(Integer, nullable=False, index=True) - acknowledgment = mapped_column(String(20)) - operation_number = mapped_column(String(14)) - bank_code = mapped_column(Integer) - cashier = mapped_column(String(2)) - date = mapped_column(Date) - time = mapped_column(Time) - shift = mapped_column(String(1)) - total_cash_paid = mapped_column(Integer) - total_contributions = mapped_column(Integer) - counter_payment = mapped_column(SmallInteger) - pece_code = mapped_column(String(5)) - payment_id = mapped_column(Integer) + id: Mapped[int] = mapped_column(Integer) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + payment_id: Mapped[int] = mapped_column(Integer) + + acknowledgment: Mapped[str] = mapped_column(String(20)) + operation_number: Mapped[str] = mapped_column(String(14)) + bank_code: Mapped[int] = mapped_column(Integer) + cashier: Mapped[str] = mapped_column(String(2)) + date: Mapped[Date2] = mapped_column(Date) + time: Mapped[Time2] = mapped_column(Time) + shift: Mapped[str] = mapped_column(String(1)) + total_cash_paid: Mapped[int] = mapped_column(Integer) + total_contributions: Mapped[int] = mapped_column(Integer) + counter_payment: Mapped[int] = mapped_column(SmallInteger) + pece_code: Mapped[str] = mapped_column(String(5)) # Timestamps created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_destination.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_destination.py index 25d72f49..ee643464 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_destination.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_destination.py @@ -1,5 +1,6 @@ +from datetime import datetime from typing import TYPE_CHECKING -from sqlalchemy import ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint +from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, func from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm.base import Mapped from core.database import Base @@ -10,19 +11,27 @@ if TYPE_CHECKING: class PedimentoRectificationDestination(Base): __tablename__ = 'pedimento_rectification_destination' __table_args__ = ( - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_rectification_destination'), PrimaryKeyConstraint('id', name='pedimento_rectification_destination_pkey'), - UniqueConstraint('pedimento_id', name='pedimento_rectification_destination_pedimento_id_key'), + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_rectification_destination_tenant'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_rectification_destination_company'), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_rectification_destination'), + UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_rectification_destination_pedimento_id_key'), {'schema': 'a76'} ) - id = mapped_column(Integer) - pedimento_id = mapped_column(Integer, nullable=False) - tenant_id = mapped_column(Integer, nullable=False, index=True) - destination_pedimento_year = mapped_column(String(2)) - destination_customs_office = mapped_column(String(3)) - destination_license = mapped_column(String(4)) - destination_pedimento_number = mapped_column(String(7)) + id: Mapped[int] = mapped_column(Integer) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + + destination_pedimento_year: Mapped[str] = mapped_column(String(2)) + destination_customs_office: Mapped[str] = mapped_column(String(3)) + destination_license: Mapped[str] = mapped_column(String(4)) + destination_pedimento_number: Mapped[str] = mapped_column(String(7)) + + # Timestamps + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) + deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_rectification_destination') \ No newline at end of file diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_origin.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_origin.py index 3a02d4b6..c0898eda 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_origin.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_origin.py @@ -1,5 +1,6 @@ +from datetime import datetime from typing import TYPE_CHECKING -from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, String, UniqueConstraint +from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, String, UniqueConstraint, func from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm.base import Mapped from core.database import Base @@ -10,28 +11,36 @@ if TYPE_CHECKING: class PedimentoRectificationOrigin(Base): __tablename__ = 'pedimento_rectification_origin' __table_args__ = ( - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_rectification_origin'), PrimaryKeyConstraint('id', name='pedimento_rectification_origin_pkey'), - UniqueConstraint('pedimento_id', name='pedimento_rectification_origin_pedimento_id_key'), + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_rectification_origin_tenant'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_rectification_origin_company'), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_rectification_origin'), + UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_rectification_origin_pedimento_id_key'), {'schema': 'a76'} ) - id = mapped_column(Integer) - pedimento_id = mapped_column(Integer, nullable=False) - tenant_id = mapped_column(Integer, nullable=False, index=True) - original_pedimento_year = mapped_column(String(2)) - original_customs_office = mapped_column(String(3)) - original_license = mapped_column(String(4)) - original_pedimento_number = mapped_column(String(7)) - original_pedimento_code = mapped_column(String(2)) - original_payment_date = mapped_column(DateTime) - total_cash = mapped_column(Integer) - total_others = mapped_column(Integer) - reason = mapped_column(String(255)) - charge_to_client = mapped_column(SmallInteger) - use_original_payment_date_for_interest_calc = mapped_column(SmallInteger) - manual_calculation = mapped_column(SmallInteger) - original_pedimento_norms = mapped_column(SmallInteger) + id: Mapped[int] = mapped_column(Integer) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + + original_pedimento_year: Mapped[str] = mapped_column(String(2)) + original_customs_office: Mapped[str] = mapped_column(String(3)) + original_license: Mapped[str] = mapped_column(String(4)) + original_pedimento_number: Mapped[str] = mapped_column(String(7)) + original_pedimento_code: Mapped[str] = mapped_column(String(2)) + original_payment_date: Mapped[datetime] = mapped_column(DateTime) + total_cash: Mapped[int] = mapped_column(Integer) + total_others: Mapped[int] = mapped_column(Integer) + reason: Mapped[str] = mapped_column(String(255)) + charge_to_client: Mapped[int] = mapped_column(SmallInteger) + use_original_payment_date_for_interest_calc: Mapped[int] = mapped_column(SmallInteger) + manual_calculation: Mapped[int] = mapped_column(SmallInteger) + original_pedimento_norms: Mapped[int] = mapped_column(SmallInteger) + + # Timestamps + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) + updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) + deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_rectification_origin') \ No newline at end of file diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_transport_means.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_transport_means.py index f17baf99..05146158 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_transport_means.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_transport_means.py @@ -1,3 +1,4 @@ +from datetime import datetime from typing import TYPE_CHECKING from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, String, UniqueConstraint, text from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -10,20 +11,23 @@ if TYPE_CHECKING: class PedimentoTransportMeans(Base): __tablename__ = 'pedimento_transport_means' __table_args__ = ( - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_transport_means'), PrimaryKeyConstraint('id', name='pedimento_transport_means_pkey'), - UniqueConstraint('pedimento_id', name='pedimento_transport_means_pedimento_id_key'), + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_transport_means_tenant'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_transport_means_company'), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_transport_means'), + UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_transport_means_pedimento_id_key'), {'schema': 'a76'} ) - id = mapped_column(Integer) - pedimento_id = mapped_column(Integer, nullable=False) - tenant_id = mapped_column(Integer, nullable=False, index=True) - destination = mapped_column(SmallInteger) - entry_exit = mapped_column(String(2)) - arrival = mapped_column(String(2)) - departure = mapped_column(String(2)) - created_at = mapped_column(DateTime, server_default=text('CURRENT_TIMESTAMP')) + id: Mapped[int] = mapped_column(Integer) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + + destination: Mapped[int] = mapped_column(SmallInteger) + entry_exit: Mapped[str] = mapped_column(String(2)) + arrival: Mapped[str] = mapped_column(String(2)) + departure: Mapped[str] = mapped_column(String(2)) + created_at: Mapped[datetime] = mapped_column(DateTime, server_default=text('CURRENT_TIMESTAMP')) pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_transport_means') \ No newline at end of file diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_validation.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_validation.py index c8f0ec68..d0cf0229 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_validation.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_validation.py @@ -11,24 +11,28 @@ if TYPE_CHECKING: class PedimentoValidation(Base): __tablename__ = 'pedimento_validation' #PedimentoValidacion __table_args__ = ( - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_validation'), PrimaryKeyConstraint('id', name='pedimento_validation_pkey'), - UniqueConstraint('pedimento_id', name='pedimento_validation_pedimento_id_key'), + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), + ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_validation'), + UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_validation_pedimento_id_key'), {'schema': 'a76'} ) - id = mapped_column(Integer) - pedimento_id = mapped_column(Integer, nullable=False) - tenant_id = mapped_column(Integer, nullable=False, index=True) - validator = mapped_column(String(3)) #validador - validation_ack = mapped_column(String(8)) #acuse_validacion - pre_ack = mapped_column(String(8)) #acuse_previo - line_signature = mapped_column(String(50)) #firma_linea_captura - electronic_signature = mapped_column(String(999)) #firma_electronica - certificate_number = mapped_column(String(99)) #numero_certificado - validator_id = mapped_column(Integer) - responsible_id = mapped_column(Integer) + id: Mapped[int] = mapped_column(Integer) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + + validator: Mapped[str] = mapped_column(String(3)) #validador + validation_ack: Mapped[str] = mapped_column(String(8)) #acuse_validacion + pre_ack: Mapped[str] = mapped_column(String(8)) #acuse_previo + line_signature: Mapped[str] = mapped_column(String(50)) #firma_linea_captura + electronic_signature: Mapped[str] = mapped_column(String(999)) #firma_electronica + certificate_number: Mapped[str] = mapped_column(String(99)) #numero_certificado + validator_id: Mapped[int] = mapped_column(Integer) + responsible_id: Mapped[int] = mapped_column(Integer) + + # Timestamps created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py b/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py index 09e13f4a..6663917b 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py @@ -1,6 +1,6 @@ from decimal import Decimal from typing import TYPE_CHECKING, Optional -from sqlalchemy import DateTime, ForeignKeyConstraint, Index, Integer, Numeric, PrimaryKeyConstraint, String, func, text +from sqlalchemy import DateTime, ForeignKeyConstraint, Index, Integer, Numeric, PrimaryKeyConstraint, String, UniqueConstraint, func, text from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm.base import Mapped from datetime import datetime @@ -28,11 +28,13 @@ if TYPE_CHECKING: class Pedimentos(Base): __tablename__ = 'pedimentos' __table_args__ = ( + PrimaryKeyConstraint('id', name='pedimentos_pkey'), + ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimentos_tenant'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimentos_company'), ForeignKeyConstraint(['client_id'], ['a76.client_provider.id'], name='fk_pedimentos_client'), ForeignKeyConstraint(['regime'], ['public.pedimento_regimens.code'], name='fk_pedimentos_regime'), - ForeignKeyConstraint(['pedimento_code'], ['public.pedimento_codes.code'], name='fk_pedimentos_code'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimentos_tenant'), - PrimaryKeyConstraint('id', name='pedimentos_pkey'), + ForeignKeyConstraint(['pedimento_code'], ['public.pedimento_codes.code'], name='fk_pedimentos_code'), + UniqueConstraint('tenant_id', 'company_id', 'year', 'customs_office', 'license', 'pedimento_number', name='pedimentos_unique_key'), Index('idx_pedimentos_client_id', 'client_id'), Index('idx_pedimentos_created_at', 'created_at'), Index('idx_pedimentos_status', 'status'), @@ -41,6 +43,8 @@ class Pedimentos(Base): id: Mapped[int] = mapped_column(Integer) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + year: Mapped[str] = mapped_column(String(2)) customs_office: Mapped[str] = mapped_column(String(2)) license: Mapped[str] = mapped_column(String(4)) diff --git a/backend/api/v1/modules/a76/permission_rule_oct/models.py b/backend/api/v1/modules/a76/permission_rule_oct/models.py index dad79c9e..aae27a27 100644 --- a/backend/api/v1/modules/a76/permission_rule_oct/models.py +++ b/backend/api/v1/modules/a76/permission_rule_oct/models.py @@ -8,14 +8,18 @@ class PermissionRuleOct(Base): __tablename__ = "permission_rule_oct" __table_args__ = ( PrimaryKeyConstraint('id', name='permission_rule_oct_pkey'), - UniqueConstraint('permission', 'tenant_id', name='permission_rule_oct_permission_tenant_ukey'), ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_permission_rule_oct_tenant'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_permission_rule_oct_company'), + UniqueConstraint('tenant_id', 'company_id', 'permission', name='permission_rule_oct_permission_tenant_ukey'), {"schema": "a76"} ) id: Mapped[int] = mapped_column(Integer, primary_key=True) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + permission: Mapped[str] = mapped_column(String(20)) start_date: Mapped[Optional[int]] = mapped_column() end_date: Mapped[Optional[int]] = mapped_column() sector: Mapped[Optional[str]] = mapped_column(String(8)) system: Mapped[Optional[str]] = mapped_column(String(5)) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) \ No newline at end of file + \ No newline at end of file diff --git a/backend/api/v1/modules/a76/seal/models.py b/backend/api/v1/modules/a76/seal/models.py index 52902408..e47b5ba2 100644 --- a/backend/api/v1/modules/a76/seal/models.py +++ b/backend/api/v1/modules/a76/seal/models.py @@ -7,11 +7,15 @@ class Seal(Base): __tablename__ = "seal" __table_args__ = ( PrimaryKeyConstraint('id', name='seal_pkey'), - UniqueConstraint('seal', name='seal_ukey'), ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_seal_tenant'), + ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_seal_company'), + UniqueConstraint('tenant_id', 'company_id', 'seal', name='seal_ukey'), {"schema": "a76"} ) id: Mapped[int] = mapped_column(Integer, primary_key=True) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + seal: Mapped[str] = mapped_column(String(15)) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) \ No newline at end of file + \ No newline at end of file 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 6e936f97..075e8e2e 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 @@ -1,8 +1,12 @@ -from typing import Optional +from typing import TYPE_CHECKING, Optional from sqlalchemy import String, Integer, ForeignKey, ForeignKeyConstraint, PrimaryKeyConstraint from sqlalchemy.orm import mapped_column, Mapped, relationship from core.database import Base +if TYPE_CHECKING: + from ..pedimento_codes.models import PedimentoCode + from ..pedimento_regimens.models import RegimenPedimento + class CodePedimentoRegimen(Base): __tablename__ = "code_pedimento_regimens" #GClavePedRegimen __table_args__ = ( 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 ee816aeb..a0e99555 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 @@ -1,8 +1,12 @@ -from typing import List +from typing import TYPE_CHECKING, List from sqlalchemy import String, PrimaryKeyConstraint from sqlalchemy.orm import mapped_column, Mapped, relationship from core.database import Base +if TYPE_CHECKING: + from ..code_pedimento_regimens.models import CodePedimentoRegimen + + class PedimentoCode(Base): __tablename__ = "pedimento_codes" # GClavePed __table_args__ = ( 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 0ddf57b9..bcd60665 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 @@ -1,8 +1,11 @@ -from typing import List +from typing import TYPE_CHECKING, List from sqlalchemy import String, PrimaryKeyConstraint, ForeignKey from sqlalchemy.orm import mapped_column, Mapped, relationship from core.database import Base +if TYPE_CHECKING: + from ..code_pedimento_regimens.models import CodePedimentoRegimen + class RegimenPedimento(Base): __tablename__ = "pedimento_regimens" #GRegimenPed __table_args__ = ( diff --git a/docs/MODULOS_A76_IMPLEMENTADOS.md b/docs/MODULOS_A76_IMPLEMENTADOS.md index e76b8329..2c63b62e 100644 --- a/docs/MODULOS_A76_IMPLEMENTADOS.md +++ b/docs/MODULOS_A76_IMPLEMENTADOS.md @@ -30,7 +30,7 @@ ## 🔗 Relaciones de Base de Datos ### Relaciones Principales -- **Part ↔ Class**: Relación de clave compuesta (client_key, part_class ↔ class_code) +- **Part ↔ Class**: Relación de clave compuesta (client_id, part_class ↔ class_code) - **Part → Country**: Clave foránea a public.countries (country_of_origin) - **Part → CurrencyType**: Clave foránea a public.currency_types (currency_key) - **Class → MaterialType**: Clave foránea a public.material_types (material_key) @@ -75,34 +75,34 @@ Part (Partes) |--------|----------|-------------| | POST | `/` | Crear parte | | GET | `/` | Listar todas con paginación y filtros | -| GET | `/client/{client_key}` | Obtener partes por cliente | +| GET | `/client/{client_id}` | Obtener partes por cliente | | GET | `/search/fraction/{fraction}` | Buscar por fracción arancelaria | | GET | `/search/supplier/{supplier}` | Buscar por proveedor | | GET | `/search/country/{country_code}` | Buscar por país | | GET | `/statistics` | Obtener estadísticas de partes | -| GET | `/{client_key}/{part_number}` | Obtener parte específica | -| PUT | `/{client_key}/{part_number}` | Actualizar parte | -| DELETE | `/{client_key}/{part_number}` | Eliminar parte | -| PATCH | `/{client_key}/{part_number}/toggle-status` | Cambiar estatus | -| GET | `/{client_key}/{part_number}/basic` | Obtener información básica | -| GET | `/{client_key}/{part_number}/regulatory` | Obtener información regulatoria | +| GET | `/{client_id}/{part_number}` | Obtener parte específica | +| PUT | `/{client_id}/{part_number}` | Actualizar parte | +| DELETE | `/{client_id}/{part_number}` | Eliminar parte | +| PATCH | `/{client_id}/{part_number}/toggle-status` | Cambiar estatus | +| GET | `/{client_id}/{part_number}/basic` | Obtener información básica | +| GET | `/{client_id}/{part_number}/regulatory` | Obtener información regulatoria | ### Módulo Clases (`/classes`) | Método | Endpoint | Descripción | |--------|----------|-------------| | POST | `/` | Crear clase | | GET | `/` | Listar todas con paginación y filtros | -| GET | `/client/{client_key}` | Obtener clases por cliente | +| GET | `/client/{client_id}` | Obtener clases por cliente | | GET | `/search/fraction/{fraction}` | Buscar por fracción arancelaria | | GET | `/search/material/{material_key}` | Buscar por material | | GET | `/search/unit-measure/{unit_of_measure}` | Buscar por unidad de medida | | GET | `/search/physical-review/{physical_review}` | Buscar por revisión física | | GET | `/statistics` | Obtener estadísticas de clases | -| GET | `/{client_key}/{class_code}` | Obtener clase específica | -| PUT | `/{client_key}/{class_code}` | Actualizar clase | -| DELETE | `/{client_key}/{class_code}` | Eliminar clase | -| GET | `/{client_key}/{class_code}/basic` | Obtener información básica | -| GET | `/{client_key}/{class_code}/tariff` | Obtener información arancelaria | +| GET | `/{client_id}/{class_code}` | Obtener clase específica | +| PUT | `/{client_id}/{class_code}` | Actualizar clase | +| DELETE | `/{client_id}/{class_code}` | Eliminar clase | +| GET | `/{client_id}/{class_code}/basic` | Obtener información básica | +| GET | `/{client_id}/{class_code}/tariff` | Obtener información arancelaria | --- diff --git a/docs/RELATIONSHIPS.md b/docs/RELATIONSHIPS.md index 78895954..4e2e35c2 100644 --- a/docs/RELATIONSHIPS.md +++ b/docs/RELATIONSHIPS.md @@ -18,7 +18,7 @@ El modelo `Part` representa las partes/componentes en los sistemas SCAII, SCAF y - Propósito: Tipo de moneda para el costo unitario 3. **Con Class (classes)** - - Campos: `(client_key, part_class)` → `(client_key, class_code)` + - Campos: `(client_id, part_class)` → `(client_id, class_code)` - Relación: Many-to-One (usando primaryjoin complejo) - Propósito: Clasificación de la parte - Atributo: `part_class_info` @@ -34,7 +34,7 @@ El modelo `Class` representa las clases de clasificación en sistemas SCAII y SC - Propósito: Tipo de material de la clase 2. **Con Part (parts)** - - Campos: `(client_key, class_code)` → `(client_key, part_class)` + - Campos: `(client_id, class_code)` → `(client_id, part_class)` - Relación: One-to-Many (inversa de la relación en Part) - Propósito: Partes que pertenecen a esta clase - Atributo: `parts` @@ -63,7 +63,7 @@ part = session.query(Part).options( joinedload(Part.currency), joinedload(Part.part_class_info).joinedload(Class.material_type) ).filter( - Part.client_key == 1, + Part.client_id == 1, Part.part_number == "PART001" ).first() @@ -78,7 +78,7 @@ print(f"Material: {part.part_class_info.material_type.description}") Los DTOs pueden incluir información relacionada: ```python class PartDetailResponseDTO(BaseModel): - client_key: int + client_id: int part_number: str description_spanish: Optional[str] country_name: Optional[str] = None diff --git a/scripts/init_first_time.sh b/scripts/init_first_time.sh new file mode 100755 index 00000000..23ce645a --- /dev/null +++ b/scripts/init_first_time.sh @@ -0,0 +1,613 @@ +#!/bin/bash + +############################################################################### +# Script de inicialización completa para Anexo76 +# +# Este script configura automáticamente: +# 1. User Profile Attributes en Keycloak (tenant_id) +# 2. Clientes de Keycloak (Backend y Frontend) +# 3. Mappers de atributos (tenant_id) +# 4. Usuario demo en Keycloak (usuario=demo, password=demo123) +# 5. Tenant y Company en PostgreSQL +# 6. Relación usuario-tenant en tabla user_tenants +# 7. Actualización del tenant_id del usuario con el valor real +# +# Requisitos: +# - Keycloak corriendo en http://localhost:8080 +# - PostgreSQL corriendo en localhost:5432 +# - Base de datos anexo76_core creada +# - jq instalado (para procesamiento JSON) +# +# Nota: El atributo tenant_id se crea con valor inicial "1" y luego +# se actualiza con el ID real del tenant creado en PostgreSQL. +############################################################################### + +set -e # Salir si hay algún error + +# Colores para output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Variables de configuración +KEYCLOAK_URL="${KEYCLOAK_URL:-http://localhost:8080}" +KEYCLOAK_ADMIN="${KEYCLOAK_ADMIN:-admin}" +KEYCLOAK_ADMIN_PASSWORD="${KEYCLOAK_ADMIN_PASSWORD:-admin}" +KEYCLOAK_REALM="${KEYCLOAK_REALM:-master}" +KEYCLOACK_ADMIN_URL="${KEYCLOACK_ADMIN_URL:-http://localhost:9000}" + +POSTGRES_HOST="${POSTGRES_HOST:-localhost}" +POSTGRES_PORT="${POSTGRES_PORT:-5432}" +POSTGRES_DB="${POSTGRES_DB:-anexo76_core}" +POSTGRES_USER="${POSTGRES_USER:-postgres}" +POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-postgres}" + +DEMO_USERNAME="demo" +DEMO_PASSWORD="demo123" +DEMO_EMAIL="demo@aduanasoft.com" +DEMO_FIRSTNAME="Demo" +DEMO_LASTNAME="User" + +TENANT_NAME="Aduanasoft" +TENANT_SLUG="aduanasoft" +COMPANY_NAME="Aduanasoft S.A. de C.V." +COMPANY_RFC="ADS010101AAA" + +echo -e "${GREEN}════════════════════════════════════════════════════════${NC}" +echo -e "${GREEN} Inicialización del Sistema Anexo76${NC}" +echo -e "${GREEN}════════════════════════════════════════════════════════${NC}" + +############################################################################### +# 1. Esperar a que Keycloak esté listo +############################################################################### +echo -e "\n${YELLOW}[1/8] Esperando a que Keycloak esté disponible...${NC}" +MAX_RETRIES=30 +RETRY_COUNT=0 + +while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do + if curl -s -f "${KEYCLOACK_ADMIN_URL}/health/ready" > /dev/null 2>&1; then + echo -e "${GREEN}✓ Keycloak está listo${NC}" + break + fi + RETRY_COUNT=$((RETRY_COUNT + 1)) + echo "Intento $RETRY_COUNT/$MAX_RETRIES..." + sleep 2 +done + +if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then + echo -e "${RED}✗ Error: Keycloak no está disponible después de $MAX_RETRIES intentos${NC}" + exit 1 +fi + +############################################################################### +# 2. Obtener token de administrador de Keycloak +############################################################################### +echo -e "\n${YELLOW}[2/8] Obteniendo token de administrador de Keycloak...${NC}" + +TOKEN_RESPONSE=$(curl -s -X POST "${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=${KEYCLOAK_ADMIN}" \ + -d "password=${KEYCLOAK_ADMIN_PASSWORD}" \ + -d "grant_type=password" \ + -d "client_id=admin-cli") + +ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | grep -o '"access_token":"[^"]*' | sed 's/"access_token":"//') + +if [ -z "$ACCESS_TOKEN" ]; then + echo -e "${RED}✗ Error: No se pudo obtener el token de acceso${NC}" + echo "Respuesta: $TOKEN_RESPONSE" + exit 1 +fi + +echo -e "${GREEN}✓ Token obtenido exitosamente${NC}" + +############################################################################### +# 3. Configurar User Profile Attributes con valores por defecto +############################################################################### +echo -e "\n${YELLOW}[3/8] Configurando User Profile Attributes...${NC}" + +# Obtener configuración actual del User Profile +USER_PROFILE=$(curl -s -X GET "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/users/profile" \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json") + +# Verificar si tenant_id existe +TENANT_ID_EXISTS=$(echo "$USER_PROFILE" | grep -q "\"name\":\"tenant_id\"" && echo "true" || echo "false") + +if [ "$TENANT_ID_EXISTS" = "false" ]; then + echo "Agregando atributo tenant_id al User Profile..." + + # Crear el atributo tenant_id con valor por defecto + UPDATED_PROFILE=$(echo "$USER_PROFILE" | jq '.attributes += [{ + "name": "tenant_id", + "displayName": "Tenant ID", + "validations": {}, + "annotations": { + "inputType": "text" + }, + "required": { + "roles": [], + "scopes": [] + }, + "permissions": { + "view": ["admin", "user"], + "edit": ["admin"] + }, + "multivalued": false, + "group": null + }]') + + # Actualizar el User Profile + curl -s -X PUT "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/users/profile" \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$UPDATED_PROFILE" + + echo -e "${GREEN}✓ Atributo tenant_id agregado al User Profile${NC}" +else + echo -e "${YELLOW}⚠ Atributo tenant_id ya existe en User Profile${NC}" +fi + +############################################################################### +# 4. Configurar clientes de Keycloak (Backend y Frontend) +############################################################################### +echo -e "\n${YELLOW}[4/8] Configurando clientes de Keycloak...${NC}" + +# 3.1 Crear cliente Backend +echo "Configurando cliente Backend..." +BACKEND_CLIENT_EXISTS=$(curl -s -X GET "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients?clientId=anexo76-backend" \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json") + +if echo "$BACKEND_CLIENT_EXISTS" | grep -q "\"clientId\":\"anexo76-backend\""; then + echo -e "${YELLOW}⚠ Cliente Backend ya existe${NC}" + BACKEND_CLIENT_ID=$(echo "$BACKEND_CLIENT_EXISTS" | grep -o '"id":"[^"]*' | head -1 | sed 's/"id":"//') +else + CREATE_BACKEND=$(curl -s -w "\n%{http_code}" -X POST "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients" \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{ + "clientId": "anexo76-backend", + "name": "Anexo76 Backend API", + "description": "Backend API para el sistema Anexo76", + "enabled": true, + "protocol": "openid-connect", + "publicClient": false, + "serviceAccountsEnabled": true, + "directAccessGrantsEnabled": true, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "rootUrl": "http://localhost:8000/api", + "baseUrl": "http://localhost:8000/api", + "redirectUris": [ + "http://localhost:8000/*", + "http://localhost:5180/*" + ], + "webOrigins": ["http://localhost:8000/api"], + "attributes": { + "access.token.lifespan": "3600", + "client.secret.creation.time": "0" + } + }') + + HTTP_CODE=$(echo "$CREATE_BACKEND" | tail -n1) + if [ "$HTTP_CODE" = "201" ]; then + echo -e "${GREEN}✓ Cliente Backend creado${NC}" + # Obtener ID del cliente recién creado + BACKEND_CLIENT_EXISTS=$(curl -s -X GET "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients?clientId=anexo76-backend" \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json") + BACKEND_CLIENT_ID=$(echo "$BACKEND_CLIENT_EXISTS" | grep -o '"id":"[^"]*' | head -1 | sed 's/"id":"//') + else + echo -e "${RED}✗ Error al crear cliente Backend (HTTP ${HTTP_CODE})${NC}" + fi +fi + +# Obtener y mostrar el secret del cliente Backend +if [ -n "$BACKEND_CLIENT_ID" ]; then + BACKEND_SECRET=$(curl -s -X GET "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients/${BACKEND_CLIENT_ID}/client-secret" \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json" | grep -o '"value":"[^"]*' | sed 's/"value":"//') + + echo -e "${GREEN}✓ Backend Client ID: anexo76-backend${NC}" + echo -e "${GREEN}✓ Backend Client Secret: ${BACKEND_SECRET}${NC}" + + # Actualizar archivo .env con el client secret + if [ -n "$BACKEND_SECRET" ]; then + ENV_FILE="" + # Buscar archivo .env en el directorio actual o en el directorio padre + if [ -f ".env" ]; then + ENV_FILE=".env" + elif [ -f "../.env" ]; then + ENV_FILE="../.env" + fi + + if [ -n "$ENV_FILE" ]; then + # Verificar si la variable ya existe en el archivo + if grep -q "^KEYCLOAK_CLIENT_SECRET=" "$ENV_FILE"; then + # Actualizar el valor existente + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS + sed -i '' "s|^KEYCLOAK_CLIENT_SECRET=.*|KEYCLOAK_CLIENT_SECRET=${BACKEND_SECRET}|" "$ENV_FILE" + else + # Linux + sed -i "s|^KEYCLOAK_CLIENT_SECRET=.*|KEYCLOAK_CLIENT_SECRET=${BACKEND_SECRET}|" "$ENV_FILE" + fi + echo -e "${GREEN}✓ KEYCLOAK_CLIENT_SECRET actualizado en ${ENV_FILE}${NC}" + else + # Agregar la variable si no existe + echo "KEYCLOAK_CLIENT_SECRET=${BACKEND_SECRET}" >> "$ENV_FILE" + echo -e "${GREEN}✓ KEYCLOAK_CLIENT_SECRET agregado a ${ENV_FILE}${NC}" + fi + else + echo -e "${YELLOW}⚠ No se encontró archivo .env, el secret debe agregarse manualmente${NC}" + fi + fi +fi + +# 3.2 Crear cliente Frontend +echo "Configurando cliente Frontend..." +FRONTEND_CLIENT_EXISTS=$(curl -s -X GET "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients?clientId=anexo76-frontend" \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json") + +if echo "$FRONTEND_CLIENT_EXISTS" | grep -q "\"clientId\":\"anexo76-frontend\""; then + echo -e "${YELLOW}⚠ Cliente Frontend ya existe${NC}" + FRONTEND_CLIENT_ID=$(echo "$FRONTEND_CLIENT_EXISTS" | grep -o '"id":"[^"]*' | head -1 | sed 's/"id":"//') +else + CREATE_FRONTEND=$(curl -s -w "\n%{http_code}" -X POST "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients" \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{ + "clientId": "anexo76-frontend", + "name": "Anexo76 Frontend", + "description": "Aplicación web frontend para el sistema Anexo76", + "enabled": true, + "protocol": "openid-connect", + "publicClient": true, + "directAccessGrantsEnabled": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "rootUrl": "http://localhost:5173", + "baseUrl": "http://localhost:5173", + "redirectUris": [ + "http://localhost:5173/*", + "http://localhost:3000/*", + "http://localhost:5180/*" + ], + "postLogoutRedirectUris": [ + "http://localhost:5173/*", + "http://localhost:3000/*", + "http://localhost:5180/*" + ], + "webOrigins": [ + "http://localhost:5173", + "http://localhost:3000", + "http://localhost:5180" + ], + "attributes": { + "pkce.code.challenge.method": "S256" + } + }') + + HTTP_CODE=$(echo "$CREATE_FRONTEND" | tail -n1) + if [ "$HTTP_CODE" = "201" ]; then + echo -e "${GREEN}✓ Cliente Frontend creado${NC}" + # Obtener ID del cliente recién creado + FRONTEND_CLIENT_EXISTS=$(curl -s -X GET "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients?clientId=anexo76-frontend" \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json") + FRONTEND_CLIENT_ID=$(echo "$FRONTEND_CLIENT_EXISTS" | grep -o '"id":"[^"]*' | head -1 | sed 's/"id":"//') + else + echo -e "${RED}✗ Error al crear cliente Frontend (HTTP ${HTTP_CODE})${NC}" + fi +fi + +############################################################################### +# 5. Configurar Mappers para tenant_id +############################################################################### +echo -e "\n${YELLOW}[5/8] Configurando mappers para tenant_id...${NC}" + +# 4.1 Configurar mapper para Backend +if [ -n "$BACKEND_CLIENT_ID" ]; then + echo "Configurando mapper para Backend..." + + # Obtener el dedicated scope del cliente backend + BACKEND_SCOPES=$(curl -s -X GET "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/clients/${BACKEND_CLIENT_ID}/optional-client-scopes" \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json") + + # Buscar el scope dedicado + BACKEND_DEDICATED_SCOPE_ID=$(curl -s -X GET "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/client-scopes" \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json" | grep -o "\"id\":\"[^\"]*\",\"name\":\"anexo76-backend-dedicated\"" | grep -o "\"id\":\"[^\"]*" | sed 's/"id":"//') + + if [ -n "$BACKEND_DEDICATED_SCOPE_ID" ]; then + # Verificar si el mapper tenant_id ya existe + MAPPER_TENANT_EXISTS=$(curl -s -X GET "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/client-scopes/${BACKEND_DEDICATED_SCOPE_ID}/protocol-mappers/models" \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json" | grep -o "\"name\":\"tenant-id-mapper\"") + + if [ -z "$MAPPER_TENANT_EXISTS" ]; then + # Crear mapper para tenant_id + curl -s -X POST "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/client-scopes/${BACKEND_DEDICATED_SCOPE_ID}/protocol-mappers/models" \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "tenant-id-mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "config": { + "user.attribute": "tenant_id", + "claim.name": "tenant_id", + "jsonType.label": "String", + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }' + echo -e "${GREEN}✓ Mapper tenant_id creado para Backend${NC}" + else + echo -e "${YELLOW}⚠ Mapper tenant_id ya existe para Backend${NC}" + fi + fi +fi + +# 4.2 Configurar mapper para Frontend +if [ -n "$FRONTEND_CLIENT_ID" ]; then + echo "Configurando mapper para Frontend..." + + # Buscar el scope dedicado del frontend + FRONTEND_DEDICATED_SCOPE_ID=$(curl -s -X GET "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/client-scopes" \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json" | grep -o "\"id\":\"[^\"]*\",\"name\":\"anexo76-frontend-dedicated\"" | grep -o "\"id\":\"[^\"]*" | sed 's/"id":"//') + + if [ -n "$FRONTEND_DEDICATED_SCOPE_ID" ]; then + # Verificar si el mapper tenant_id ya existe + MAPPER_TENANT_EXISTS=$(curl -s -X GET "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/client-scopes/${FRONTEND_DEDICATED_SCOPE_ID}/protocol-mappers/models" \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json" | grep -o "\"name\":\"tenant-id-mapper\"") + + if [ -z "$MAPPER_TENANT_EXISTS" ]; then + # Crear mapper para tenant_id + curl -s -X POST "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/client-scopes/${FRONTEND_DEDICATED_SCOPE_ID}/protocol-mappers/models" \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "tenant-id-mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "config": { + "user.attribute": "tenant_id", + "claim.name": "tenant_id", + "jsonType.label": "String", + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }' + echo -e "${GREEN}✓ Mapper tenant_id creado para Frontend${NC}" + else + echo -e "${YELLOW}⚠ Mapper tenant_id ya existe para Frontend${NC}" + fi + fi +fi + +############################################################################### +# 6. Crear usuario demo en Keycloak +############################################################################### +echo -e "\n${YELLOW}[6/8] Creando usuario demo en Keycloak...${NC}" + +# Verificar si el usuario ya existe +USER_EXISTS=$(curl -s -X GET "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/users?username=${DEMO_USERNAME}" \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json") + +if echo "$USER_EXISTS" | grep -q "\"username\":\"${DEMO_USERNAME}\""; then + echo -e "${YELLOW}⚠ Usuario demo ya existe, actualizando...${NC}" + USER_ID=$(echo "$USER_EXISTS" | grep -o '"id":"[^"]*' | head -1 | sed 's/"id":"//') + + # Actualizar usuario existente + curl -s -X PUT "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/users/${USER_ID}" \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"enabled\": true, + \"firstName\": \"${DEMO_FIRSTNAME}\", + \"lastName\": \"${DEMO_LASTNAME}\", + \"email\": \"${DEMO_EMAIL}\", + \"emailVerified\": true + }" + + # Resetear contraseña + curl -s -X PUT "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/users/${USER_ID}/reset-password" \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"type\": \"password\", + \"value\": \"${DEMO_PASSWORD}\", + \"temporary\": false + }" + + echo -e "${GREEN}✓ Usuario demo actualizado${NC}" +else + # Crear nuevo usuario con tenant_id por defecto + CREATE_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/users" \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"username\": \"${DEMO_USERNAME}\", + \"enabled\": true, + \"firstName\": \"${DEMO_FIRSTNAME}\", + \"lastName\": \"${DEMO_LASTNAME}\", + \"email\": \"${DEMO_EMAIL}\", + \"emailVerified\": true, + \"attributes\": { + \"tenant_id\": [\"1\"] + }, + \"credentials\": [{ + \"type\": \"password\", + \"value\": \"${DEMO_PASSWORD}\", + \"temporary\": false + }] + }") + + HTTP_CODE=$(echo "$CREATE_RESPONSE" | tail -n1) + + if [ "$HTTP_CODE" = "201" ]; then + echo -e "${GREEN}✓ Usuario demo creado exitosamente${NC}" + + # Obtener el ID del usuario recién creado + USER_RESPONSE=$(curl -s -X GET "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/users?username=${DEMO_USERNAME}" \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H "Content-Type: application/json") + USER_ID=$(echo "$USER_RESPONSE" | grep -o '"id":"[^"]*' | head -1 | sed 's/"id":"//') + else + echo -e "${RED}✗ Error al crear usuario (HTTP ${HTTP_CODE})${NC}" + echo "Respuesta: $CREATE_RESPONSE" + exit 1 + fi +fi + +echo -e "${GREEN}✓ Usuario: ${DEMO_USERNAME}${NC}" +echo -e "${GREEN}✓ Password: ${DEMO_PASSWORD}${NC}" +echo -e "${GREEN}✓ Keycloak User ID: ${USER_ID}${NC}" + +############################################################################### +# 7. Agregar atributo tenant_id al usuario +############################################################################### +echo -e "\n${YELLOW}[7/8] Configurando atributo tenant_id para usuario demo...${NC}" + +# Nota: El tenant_id se agregará después de crear el tenant en PostgreSQL + +############################################################################### +# 8. Crear tenant y company en PostgreSQL +############################################################################### +echo -e "\n${YELLOW}[8/8] Creando tenant y company en PostgreSQL...${NC}" + +# Esperar a que PostgreSQL esté listo +echo "Esperando a que PostgreSQL esté disponible..." +MAX_PG_RETRIES=30 +PG_RETRY_COUNT=0 + +while [ $PG_RETRY_COUNT -lt $MAX_PG_RETRIES ]; do + if PGPASSWORD="${POSTGRES_PASSWORD}" psql -h "${POSTGRES_HOST}" -p "${POSTGRES_PORT}" -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -c "SELECT 1;" > /dev/null 2>&1; then + echo -e "${GREEN}✓ PostgreSQL está listo${NC}" + break + fi + PG_RETRY_COUNT=$((PG_RETRY_COUNT + 1)) + echo "Intento $PG_RETRY_COUNT/$MAX_PG_RETRIES..." + sleep 2 +done + +if [ $PG_RETRY_COUNT -eq $MAX_PG_RETRIES ]; then + echo -e "${RED}✗ Error: PostgreSQL no está disponible${NC}" + exit 1 +fi + +# Insertar o actualizar tenant +PGPASSWORD="${POSTGRES_PASSWORD}" psql -h "${POSTGRES_HOST}" -p "${POSTGRES_PORT}" -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" <