Merge pull request 'Refactor database models to include company_id and update unique constraints' (#12) from feature/catalogos-generales into development

Reviewed-on: ADUANASOFT/anexo76#12
This commit is contained in:
2025-11-08 00:48:33 +00:00
41 changed files with 1185 additions and 418 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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")

View File

@@ -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"<Class(client_key={self.client_key}, class_code='{self.class_code}', description='{self.description_es}')>"
return f"<Class(client_id={self.client_id}, class_code='{self.class_code}', description='{self.description_es}')>"

View File

@@ -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,

View File

@@ -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 = {}

View File

@@ -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")

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)
foreign_currency: Mapped[Optional[str]] = mapped_column(String(7))

View File

@@ -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)

View File

@@ -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))

View File

@@ -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")

View File

@@ -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"<Part(client_key={self.client_key}, part_number='{self.part_number}', description='{self.description_spanish}')>"
return f"<Part(client_id={self.client_id}, part_number='{self.part_number}', description='{self.description_spanish}')>"

View File

@@ -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,

View File

@@ -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,

View File

@@ -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())

View File

@@ -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())

View File

@@ -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())

View File

@@ -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())

View File

@@ -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())

View File

@@ -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())

View File

@@ -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())

View File

@@ -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())

View File

@@ -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())

View File

@@ -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())

View File

@@ -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())

View File

@@ -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())

View File

@@ -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')

View File

@@ -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')

View File

@@ -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')

View File

@@ -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)

View File

@@ -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))

View File

@@ -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)

View File

@@ -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)

View File

@@ -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__ = (

View File

@@ -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__ = (

View File

@@ -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__ = (

View File

@@ -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 |
---

View File

@@ -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

613
scripts/init_first_time.sh Executable file
View File

@@ -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}" <<EOF
INSERT INTO a76.tenants (name, slug, type, keycloak_realm, contact_email, is_active, created_at, updated_at)
VALUES ('${TENANT_NAME}', '${TENANT_SLUG}', 'SHARED', '${KEYCLOAK_REALM}', '${DEMO_EMAIL}', true, now(), now())
ON CONFLICT (slug)
DO UPDATE SET
name = EXCLUDED.name,
contact_email = EXCLUDED.contact_email,
updated_at = CURRENT_TIMESTAMP;
EOF
# Obtener el ID del tenant
TENANT_ID=$(PGPASSWORD="${POSTGRES_PASSWORD}" psql -h "${POSTGRES_HOST}" -p "${POSTGRES_PORT}" -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -t -c "SELECT id FROM a76.tenants WHERE slug = '${TENANT_SLUG}';")
TENANT_ID=$(echo "$TENANT_ID" | xargs) # Trim whitespace
if [ -z "$TENANT_ID" ]; then
echo -e "${RED}✗ Error: No se pudo obtener el ID del tenant${NC}"
exit 1
fi
echo -e "${GREEN}✓ Tenant ID: ${TENANT_ID}${NC}"
# Insertar company si no existe
COMPANY_EXISTS=$(PGPASSWORD="${POSTGRES_PASSWORD}" psql -h "${POSTGRES_HOST}" -p "${POSTGRES_PORT}" -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -t -c "SELECT COUNT(*) FROM a76.company WHERE tenant_id = ${TENANT_ID};")
COMPANY_EXISTS=$(echo "$COMPANY_EXISTS" | xargs)
if [ "$COMPANY_EXISTS" = "0" ]; then
PGPASSWORD="${POSTGRES_PASSWORD}" psql -h "${POSTGRES_HOST}" -p "${POSTGRES_PORT}" -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" <<EOF
INSERT INTO a76.company (tenant_id, name, rfc, is_service_company)
VALUES (${TENANT_ID}, '${COMPANY_NAME}', '${COMPANY_RFC}', false);
EOF
echo -e "${GREEN}✓ Company creada${NC}"
else
echo -e "${YELLOW}⚠ Company ya existe para este tenant${NC}"
fi
# Obtener información de la company
COMPANY_INFO=$(PGPASSWORD="${POSTGRES_PASSWORD}" psql -h "${POSTGRES_HOST}" -p "${POSTGRES_PORT}" -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -t -c "SELECT id, name FROM a76.company WHERE tenant_id = ${TENANT_ID} LIMIT 1;")
echo -e "${GREEN}✓ Company: ${COMPANY_INFO}${NC}"
# Agregar tenant_id al usuario demo en Keycloak
echo -e "\n${YELLOW}Asignando tenant_id al usuario demo...${NC}"
curl -s -X PUT "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/users/${USER_ID}" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"attributes\": {
\"tenant_id\": [\"${TENANT_ID}\"]
}
}"
echo -e "${GREEN}✓ Atributo tenant_id asignado al usuario${NC}"
# Agregar relación usuario-tenant en la base de datos
echo -e "\n${YELLOW}Creando relación usuario-tenant en la base de datos...${NC}"
PGPASSWORD="${POSTGRES_PASSWORD}" psql -h "${POSTGRES_HOST}" -p "${POSTGRES_PORT}" -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" <<EOF
INSERT INTO a76.user_tenants (keycloak_user_id, tenant_id, role, is_active, created_at, updated_at)
VALUES ('${USER_ID}', ${TENANT_ID}, 'admin', true, now(), now())
ON CONFLICT (keycloak_user_id, tenant_id)
DO UPDATE SET
is_active = true,
updated_at = CURRENT_TIMESTAMP;
EOF
echo -e "${GREEN}✓ Relación usuario-tenant creada en la base de datos${NC}"
###############################################################################
# Resumen final
###############################################################################
echo -e "\n${GREEN}════════════════════════════════════════════════════════${NC}"
echo -e "${GREEN} ✓ Inicialización completada exitosamente${NC}"
echo -e "${GREEN}════════════════════════════════════════════════════════${NC}"
echo -e "\n${YELLOW}Credenciales de acceso:${NC}"
echo -e " Usuario: ${GREEN}${DEMO_USERNAME}${NC}"
echo -e " Password: ${GREEN}${DEMO_PASSWORD}${NC}"
echo -e " Email: ${GREEN}${DEMO_EMAIL}${NC}"
echo -e "\n${YELLOW}Información del sistema:${NC}"
echo -e " Tenant: ${GREEN}${TENANT_NAME} (ID: ${TENANT_ID})${NC}"
echo -e " Realm: ${GREEN}${KEYCLOAK_REALM}${NC}"
echo -e " Keycloak URL: ${GREEN}${KEYCLOAK_URL}${NC}"
echo -e "\n${YELLOW}Clientes Keycloak configurados:${NC}"
echo -e " Backend Client ID: ${GREEN}anexo76-backend${NC}"
if [ -n "$BACKEND_SECRET" ]; then
echo -e " Backend Secret: ${GREEN}${BACKEND_SECRET}${NC}"
if [ -n "$ENV_FILE" ]; then
echo -e " ${GREEN}${NC} KEYCLOAK_CLIENT_SECRET actualizado en ${ENV_FILE}"
else
echo -e " ${YELLOW}${NC} Agregar manualmente al archivo .env como KEYCLOAK_CLIENT_SECRET"
fi
fi
echo -e " Frontend Client ID: ${GREEN}anexo76-frontend${NC}"
echo -e "\n${YELLOW}User Profile Attributes configurados:${NC}"
echo -e " ${GREEN}${NC} tenant_id (opcional, valor inicial: 1)"
echo -e "\n${YELLOW}Mappers configurados:${NC}"
echo -e " ${GREEN}${NC} tenant_id mapper para Backend"
echo -e " ${GREEN}${NC} tenant_id mapper para Frontend"
echo -e "\n${YELLOW}Configuración del usuario demo:${NC}"
echo -e " ${GREEN}${NC} tenant_id actualizado al ID real: ${TENANT_ID}"
echo -e " ${GREEN}${NC} Relación usuario-tenant creada (rol: admin)"
echo -e "\n${YELLOW}Puedes acceder al sistema en:${NC}"
echo -e " ${GREEN}http://localhost:5180${NC}"
echo -e "\n${GREEN}════════════════════════════════════════════════════════${NC}\n"