Refactor backend and frontend code for improved structure and functionality

- Rearranged imports in multiple files for consistency and clarity.
- Updated logging middleware to exclude specific paths from logging.
- Enhanced security module by cleaning up token handling and improving tenant validation.
- Added tenant and company scoped mixins for better database model management.
- Implemented generic CRUD routes for tenant-scoped resources.
- Improved error handling and response management in API routes.
- Cleaned up login and logout processes to ensure proper session management.
- Introduced mechanisms to clear local storage and cookies on tenant change.
- Enhanced company store to detect tenant changes and clear data accordingly.
- Added new DTO mixins for currency and value affect flags.
This commit is contained in:
2025-11-11 17:20:47 -06:00
parent ac7f8d19d8
commit b68c4316ff
247 changed files with 2248 additions and 2504 deletions

View File

@@ -1,11 +1,14 @@
import importlib.util
import logging
import os
import sys
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from urllib.parse import quote_plus
from alembic import context
from core.config import settings
import logging
from core.database import Base
from sqlalchemy import engine_from_config, pool
logger = logging.getLogger(__name__)
@@ -65,15 +68,10 @@ config.set_main_option("sqlalchemy.url", database_url)
if config.config_file_name is not None:
fileConfig(config.config_file_name)
import sys
import importlib.util
# Ajusta la ruta para que puedas importar core y módulos
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, BASE_DIR)
from core.database import Base
# Configuración de Alembic
config = context.config
fileConfig(config.config_file_name)

View File

@@ -6,11 +6,13 @@ Create Date: 2025-10-19 18:23:39.613953
"""
# pylint: disable=no-member
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "531bf8cdae06"

View File

@@ -6,17 +6,11 @@ Create Date: 2025-10-19 18:23:55.258800
"""
# pylint: disable=no-member
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from api.v1.modules.public.reference_data.pedimento_codes.seed import (
seed as pedimento_codes_seed,
)
from api.v1.modules.public.reference_data.pedimento_regimens.seed import (
seed as pedimento_regimens_seed,
)
from api.v1.modules.public.reference_data.code_pedimento_regimens.seed import (
seed as code_pedimento_regimens_seed,
)
@@ -43,6 +37,12 @@ from api.v1.modules.public.reference_data.material_types.seed import (
from api.v1.modules.public.reference_data.payment_methods.seed import (
seed as payment_methods_seed,
)
from api.v1.modules.public.reference_data.pedimento_codes.seed import (
seed as pedimento_codes_seed,
)
from api.v1.modules.public.reference_data.pedimento_regimens.seed import (
seed as pedimento_regimens_seed,
)
from api.v1.modules.public.reference_data.sectors.seed import seed as sectors_seed
from api.v1.modules.public.reference_data.transport_modes.seed import (
seed as transport_modes_seed,

View File

@@ -0,0 +1,30 @@
from datetime import datetime
from sqlalchemy import DateTime, Integer
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql import func
class TimestampMixin:
"""Mixin for common timestamp fields"""
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)
class TenantScopedMixin:
"""Mixin for tenant and company scoped entities"""
tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
class PedimentoRelatedMixin(TenantScopedMixin):
"""Mixin for entities related to pedimentos"""
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)

View File

@@ -0,0 +1,121 @@
from typing import Any, Callable, Generic, Type, TypeVar
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.orm import Session
ModelType = TypeVar("ModelType")
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
ResponseSchemaType = TypeVar("ResponseSchemaType", bound=BaseModel)
class CRUDRouterFactory(
Generic[ModelType, CreateSchemaType, UpdateSchemaType, ResponseSchemaType]
):
"""Factory to create standard CRUD routes"""
def __init__(
self,
model: Type[ModelType],
create_schema: Type[CreateSchemaType],
update_schema: Type[UpdateSchemaType],
response_schema: Type[ResponseSchemaType],
db_dependency: Callable,
auth_dependency: Callable,
prefix: str,
tags: list[str],
id_field: str = "key",
):
self.model = model
self.create_schema = create_schema
self.update_schema = update_schema
self.response_schema = response_schema
self.db_dependency = db_dependency
self.auth_dependency = auth_dependency
self.id_field = id_field
self.router = APIRouter(prefix=prefix, tags=tags)
self._register_routes()
def _register_routes(self):
"""Register all CRUD routes"""
@self.router.get("/", response_model=list[self.response_schema])
def list_items(
skip: int = 0,
limit: int = 100,
db: Session = Depends(self.db_dependency),
current_user: dict = Depends(self.auth_dependency),
):
items = db.query(self.model).offset(skip).limit(limit).all()
return items
@self.router.get(f"/{{{self.id_field}}}", response_model=self.response_schema)
def get_item(
db: Session = Depends(self.db_dependency),
current_user: dict = Depends(self.auth_dependency),
**kwargs,
):
item_id = kwargs.get(self.id_field)
obj = (
db.query(self.model)
.filter(getattr(self.model, self.id_field) == item_id)
.first()
)
if not obj:
raise HTTPException(status_code=404, detail="Not found")
return obj
@self.router.post("/", response_model=self.response_schema)
def create_item(
data: Any,
db: Session = Depends(self.db_dependency),
current_user: dict = Depends(self.auth_dependency),
):
obj = self.model(**data.dict())
db.add(obj)
db.commit()
db.refresh(obj)
return obj
@self.router.put(f"/{{{self.id_field}}}", response_model=self.response_schema)
def update_item(
data: Any,
db: Session = Depends(self.db_dependency),
current_user: dict = Depends(self.auth_dependency),
**kwargs,
):
item_id = kwargs.get(self.id_field)
obj = (
db.query(self.model)
.filter(getattr(self.model, self.id_field) == item_id)
.first()
)
if not obj:
raise HTTPException(status_code=404, detail="Not found")
for field, value in data.dict(exclude_unset=True).items():
setattr(obj, field, value)
db.commit()
db.refresh(obj)
return obj
@self.router.delete(f"/{{{self.id_field}}}", status_code=204)
def delete_item(
db: Session = Depends(self.db_dependency),
current_user: dict = Depends(self.auth_dependency),
**kwargs,
):
item_id = kwargs.get(self.id_field)
obj = (
db.query(self.model)
.filter(getattr(self.model, self.id_field) == item_id)
.first()
)
if not obj:
raise HTTPException(status_code=404, detail="Not found")
db.delete(obj)
db.commit()
return None

View File

@@ -0,0 +1,31 @@
from decimal import Decimal
from typing import Optional
from pydantic import BaseModel, Field
class CurrencyMixin(BaseModel):
"""Mixin for currency-related fields"""
currency: Optional[str] = Field(None, max_length=3, description="Currency")
currency_factor: Optional[Decimal] = Field(None, description="Currency factor")
class AffectValueMixin(BaseModel):
"""Mixin for value affect flags"""
not_affect_usd_value: Optional[int] = Field(
None, description="Not affect USD value"
)
not_affect_customs_value: Optional[int] = Field(
None, description="Not affect customs value"
)
class UpdateFlagsMixin(BaseModel):
"""Mixin for update flags"""
update_vat: Optional[int] = Field(None, description="Update VAT")
update_advalorem: Optional[int] = Field(None, description="Update advalorem")
update_cc: Optional[int] = Field(None, description="Update CC")
update_ieps: Optional[int] = Field(None, description="Update IEPS")

View File

@@ -0,0 +1,350 @@
from typing import Any, Callable, Dict, Generic, Optional, Type, TypeVar
from core.database import get_core_db
from core.security import get_current_user, validate_access_to_resource
from fastapi import APIRouter, Depends, HTTPException, Path, Query
from pydantic import BaseModel
from sqlalchemy.orm import Session
# Type variables for generic types
ModelType = TypeVar("ModelType")
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
ResponseSchemaType = TypeVar("ResponseSchemaType", bound=BaseModel)
ServiceType = TypeVar("ServiceType")
class TenantCRUDRoutes(
Generic[CreateSchemaType, UpdateSchemaType, ResponseSchemaType, ServiceType]
):
"""
Generic CRUD routes factory for tenant-scoped resources
Supports both parent resources (with list/pagination) and child resources (nested under parent).
Usage examples:
1. Parent resource with list (e.g., /pedimentos):
router = TenantCRUDRoutes(
service=PedimentosService,
create_schema=PedimentosCreate,
update_schema=PedimentosUpdate,
response_schema=PedimentosResponse,
prefix="/pedimentos",
tags=["Pedimentos"],
resource_name="Pedimento",
id_name="pedimento_id",
enable_list=True,
).router
2. Child resource (e.g., /pedimentos/{pedimento_id}/config-additional):
router = TenantCRUDRoutes(
service=PedimentoConfigAdditionalService,
create_schema=PedimentoConfigAdditionalCreate,
update_schema=PedimentoConfigAdditionalUpdate,
response_schema=PedimentoConfigAdditionalResponse,
prefix="/{pedimento_id}/config-additional",
tags=["Pedimento Config Additional"],
resource_name="Config additional",
parent_id_name="pedimento_id",
enable_list=False,
).router
"""
def __init__(
self,
service: Type[ServiceType],
create_schema: Type[CreateSchemaType],
update_schema: Type[UpdateSchemaType],
response_schema: Type[ResponseSchemaType],
prefix: str,
tags: list[str],
resource_name: str = "Resource",
id_name: Optional[str] = None, # For parent resources (e.g., "pedimento_id")
parent_id_name: Optional[
str
] = None, # For child resources (e.g., "pedimento_id")
db_dependency: Callable = get_core_db,
auth_dependency: Callable = get_current_user,
validate_parent_match: bool = True, # Validate parent_id matches in create
enable_list: bool = False, # Enable GET list endpoint with pagination
enable_filters: bool = False, # Enable custom filters in list endpoint
default_page_size: int = 50,
max_page_size: int = 100,
):
self.service = service
self.create_schema = create_schema
self.update_schema = update_schema
self.response_schema = response_schema
self.resource_name = resource_name
self.id_name = id_name or parent_id_name or "id"
self.parent_id_name = parent_id_name
self.db_dependency = db_dependency
self.auth_dependency = auth_dependency
self.validate_parent_match = validate_parent_match
self.enable_list = enable_list
self.enable_filters = enable_filters
self.default_page_size = default_page_size
self.max_page_size = max_page_size
self.router = APIRouter(prefix=prefix, tags=tags)
self._register_routes()
def _register_routes(self):
"""Register all CRUD routes"""
# LIST route (optional, for parent resources)
if self.enable_list:
if self.enable_filters:
@self.router.get("/", response_model=Dict[str, Any])
async def list_resources(
company_id: int = Query(..., description="Company ID"),
page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(
self.default_page_size,
ge=1,
le=self.max_page_size,
description="Page size",
),
status: Optional[str] = Query(None, description="Filter by status"),
db: Session = Depends(self.db_dependency),
current_user: Dict[str, Any] = Depends(self.auth_dependency),
):
f"""List all {self.resource_name}s with pagination"""
tenant_id = validate_access_to_resource(
db, company_id, current_user
)
skip = (page - 1) * page_size
filters = {}
if status:
filters["status"] = status
items, total = self.service.get_all(
db, tenant_id, company_id, skip, page_size, filters
)
return {
"items": [
self.response_schema.model_validate(item) for item in items
],
"total": total,
"page": page,
"page_size": page_size,
}
else:
@self.router.get("/", response_model=Dict[str, Any])
async def list_resources(
company_id: int = Query(..., description="Company ID"),
page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(
self.default_page_size,
ge=1,
le=self.max_page_size,
description="Page size",
),
db: Session = Depends(self.db_dependency),
current_user: Dict[str, Any] = Depends(self.auth_dependency),
):
f"""List all {self.resource_name}s with pagination"""
tenant_id = validate_access_to_resource(
db, company_id, current_user
)
skip = (page - 1) * page_size
items, total = self.service.get_all(
db, tenant_id, company_id, skip, page_size, None
)
return {
"items": [
self.response_schema.model_validate(item) for item in items
],
"total": total,
"page": page,
"page_size": page_size,
}
# GET single resource route
# For parent resources: GET /{id}
# For child resources: GET / (parent_id comes from path)
if self.parent_id_name:
# Child resource - single GET without ID in path
@self.router.get("/", response_model=self.response_schema)
async def get_resource(
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(self.db_dependency),
current_user: Dict[str, Any] = Depends(self.auth_dependency),
**path_params,
):
f"""Get {self.resource_name} by {self.parent_id_name}"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
parent_id = path_params.get(self.parent_id_name)
# Try method with 4 params (pedimento_id, tenant_id, company_id)
if hasattr(self.service, "get_by_pedimento_id"):
resource = self.service.get_by_pedimento_id(
db, parent_id, tenant_id, company_id
)
# Fallback to method with 3 params
elif hasattr(self.service, "get_by_id"):
resource = self.service.get_by_id(
db, parent_id, tenant_id, company_id
)
else:
resource = self.service.get(db, parent_id, tenant_id, company_id)
if not resource:
raise HTTPException(
status_code=404, detail=f"{self.resource_name} not found"
)
return resource
else:
# Parent resource - GET by ID in path
@self.router.get(
f"/{{{self.id_name}}}", response_model=self.response_schema
)
async def get_resource_by_id(
resource_id: int = Path(..., alias=self.id_name),
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(self.db_dependency),
current_user: Dict[str, Any] = Depends(self.auth_dependency),
):
f"""Get {self.resource_name} by ID"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
resource = self.service.get_by_id(
db, resource_id, tenant_id, company_id
)
if not resource:
raise HTTPException(
status_code=404, detail=f"{self.resource_name} not found"
)
return resource
# POST route
@self.router.post("/", response_model=self.response_schema, status_code=201)
async def create_resource(
data: CreateSchemaType,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(self.db_dependency),
current_user: Dict[str, Any] = Depends(self.auth_dependency),
**path_params,
):
f"""Create {self.resource_name}"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
# Validate parent ID match if enabled and parent_id_name exists
if self.validate_parent_match and self.parent_id_name:
parent_id = path_params.get(self.parent_id_name)
data_parent_id = getattr(data, self.parent_id_name, None)
if data_parent_id is not None and data_parent_id != parent_id:
raise HTTPException(
status_code=400,
detail=f"{self.parent_id_name.replace('_', ' ').title()} mismatch",
)
resource = self.service.create(db, data, tenant_id, company_id)
return resource
# PUT route
# For parent resources: PUT /{id}
# For child resources: PUT / (parent_id comes from path)
if self.parent_id_name:
# Child resource
@self.router.put("/", response_model=self.response_schema)
async def update_resource(
data: UpdateSchemaType,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(self.db_dependency),
current_user: Dict[str, Any] = Depends(self.auth_dependency),
**path_params,
):
f"""Update {self.resource_name}"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
parent_id = path_params.get(self.parent_id_name)
resource = self.service.update(
db, parent_id, tenant_id, company_id, data
)
if not resource:
raise HTTPException(
status_code=404, detail=f"{self.resource_name} not found"
)
return resource
else:
# Parent resource
@self.router.put(
f"/{{{self.id_name}}}", response_model=self.response_schema
)
async def update_resource_by_id(
data: UpdateSchemaType,
resource_id: int = Path(..., alias=self.id_name),
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(self.db_dependency),
current_user: Dict[str, Any] = Depends(self.auth_dependency),
):
f"""Update {self.resource_name}"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
resource = self.service.update(
db, resource_id, tenant_id, company_id, data
)
if not resource:
raise HTTPException(
status_code=404, detail=f"{self.resource_name} not found"
)
return resource
# DELETE route
# For parent resources: DELETE /{id}
# For child resources: DELETE / (parent_id comes from path)
if self.parent_id_name:
# Child resource
@self.router.delete("/", status_code=204)
async def delete_resource(
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(self.db_dependency),
current_user: Dict[str, Any] = Depends(self.auth_dependency),
**path_params,
):
f"""Delete {self.resource_name}"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
parent_id = path_params.get(self.parent_id_name)
success = self.service.delete(db, parent_id, tenant_id, company_id)
if not success:
raise HTTPException(
status_code=404, detail=f"{self.resource_name} not found"
)
return None
else:
# Parent resource
@self.router.delete(f"/{{{self.id_name}}}", status_code=204)
async def delete_resource_by_id(
resource_id: int = Path(..., alias=self.id_name),
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(self.db_dependency),
current_user: Dict[str, Any] = Depends(self.auth_dependency),
):
f"""Delete {self.resource_name}"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
success = self.service.delete(db, resource_id, tenant_id, company_id)
if not success:
raise HTTPException(
status_code=404, detail=f"{self.resource_name} not found"
)
return None

View File

@@ -1,4 +1,7 @@
from decimal import Decimal
from api.v1.common.base_models import TenantScopedMixin
from core.database import Base
from sqlalchemy import (
Boolean,
ForeignKeyConstraint,
@@ -8,12 +11,9 @@ from sqlalchemy import (
String,
)
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm.base import Mapped
from core.database import Base
class QClasses(Base):
class QClasses(Base, TenantScopedMixin):
__tablename__ = "q_classes" # QClases
__table_args__ = (
PrimaryKeyConstraint("id", name="qclases_pk"),
@@ -29,8 +29,6 @@ class QClasses(Base):
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

View File

@@ -1,16 +1,10 @@
from sqlalchemy import (
ForeignKeyConstraint,
Integer,
PrimaryKeyConstraint,
String,
UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm.base import Mapped
from api.v1.common.base_models import TenantScopedMixin
from core.database import Base
from sqlalchemy import ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String
from sqlalchemy.orm import Mapped, mapped_column
class SClasses(Base):
class SClasses(Base, TenantScopedMixin):
__tablename__ = "s_classes" # SClases
__table_args__ = (
ForeignKeyConstraint(
@@ -27,8 +21,6 @@ class SClasses(Base):
)
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

@@ -1,5 +1,7 @@
from typing import Optional
from pydantic import BaseModel
from typing import Optional, List
class CustomsBrokerDTO(BaseModel):
type: Optional[str]

View File

@@ -1,9 +1,20 @@
from sqlalchemy import Column, String, Integer, ForeignKey
from sqlalchemy.orm import relationship
from api.v1.common.base_models import TenantScopedMixin
from core.database import Base
from sqlalchemy import Column, ForeignKey, ForeignKeyConstraint, Integer, String
from sqlalchemy.orm import relationship
class CustomsBroker(Base):
__tablename__ = "GAAduanal"
class CustomsBroker(Base, TenantScopedMixin):
__tablename__ = "customs_brokers"
__table_args__ = (
ForeignKeyConstraint(
["tenant_id"], ["a76.tenants.id"], name="fk_customs_brokers_tenants"
),
ForeignKeyConstraint(
["company_id"], ["a76.company.id"], name="fk_customs_brokers_company"
),
{"schema": "a76"},
)
type = Column(String(9), nullable=True)
broker_key = Column(String(5), primary_key=True, nullable=False)
@@ -22,17 +33,23 @@ class CustomsBroker(Base):
license = Column(String(4), nullable=True)
company = Column(String(200), nullable=True)
contact = Column(String(80), nullable=True)
tenant_id = Column(String(36), nullable=False)
company_id = Column(String(36), nullable=False)
vu = relationship("CustomsBrokerVU", back_populates="customs_broker", cascade="all, delete")
personnel = relationship("CustomsBrokerPersonnel", back_populates="customs_broker", cascade="all, delete")
vu = relationship(
"CustomsBrokerVU", back_populates="customs_broker", cascade="all, delete"
)
personnel = relationship(
"CustomsBrokerPersonnel", back_populates="customs_broker", cascade="all, delete"
)
class CustomsBrokerVU(Base):
__tablename__ = "GAAduanal_VU"
__tablename__ = "customs_brokers_vu"
broker_key = Column(String(5), ForeignKey("GAAduanal.broker_key", ondelete="CASCADE"), primary_key=True)
broker_key = Column(
String(5),
ForeignKey("a76.customs_brokers.broker_key", ondelete="CASCADE"),
primary_key=True,
)
certificate_path = Column(String(1499), nullable=True)
key_path = Column(String(1499), nullable=True)
access_key = Column(String(50), nullable=True)
@@ -57,9 +74,13 @@ class CustomsBrokerVU(Base):
class CustomsBrokerPersonnel(Base):
__tablename__ = "GAAduanalPersonal"
__tablename__ = "customs_brokers_personnel"
broker_key = Column(String(5), ForeignKey("GAAduanal.broker_key", ondelete="CASCADE"), primary_key=True)
broker_key = Column(
String(5),
ForeignKey("a76.customs_brokers.broker_key", ondelete="CASCADE"),
primary_key=True,
)
line = Column(Integer, primary_key=True, nullable=False)
name = Column(String(80), nullable=True)
tax_id = Column(String(30), nullable=True)

View File

@@ -1,10 +1,12 @@
from core.database import get_core_db
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from . import services, dto
from core.database import get_core_db
from . import dto, services
router = APIRouter()
@router.get("/customs-broker/{broker_key}", response_model=dto.CustomsBrokerDTO)
def get_customs_broker(broker_key: str, db: Session = Depends(get_core_db)):
broker = services.CustomsBrokerService.get_by_broker_key(db, broker_key)
@@ -12,10 +14,14 @@ def get_customs_broker(broker_key: str, db: Session = Depends(get_core_db)):
raise HTTPException(status_code=404, detail="Customs Broker not found")
return broker
@router.post("/customs-broker", response_model=dto.CustomsBrokerDTO)
def create_customs_broker(broker_data: dto.CustomsBrokerDTO, db: Session = Depends(get_core_db)):
def create_customs_broker(
broker_data: dto.CustomsBrokerDTO, db: Session = Depends(get_core_db)
):
return services.CustomsBrokerService.create_customs_broker(db, broker_data)
@router.delete("/customs-broker/{broker_key}", response_model=dto.CustomsBrokerDTO)
def delete_customs_broker(broker_key: str, db: Session = Depends(get_core_db)):
broker = services.CustomsBrokerService.delete_customs_broker(db, broker_key)
@@ -23,16 +29,36 @@ def delete_customs_broker(broker_key: str, db: Session = Depends(get_core_db)):
raise HTTPException(status_code=404, detail="Customs Broker not found")
return broker
@router.put("/customs-broker-vu/{broker_key}", response_model=dto.CustomsBrokerVUCreateDTO)
def update_customs_broker_vu(broker_key: str, vu_data: dto.CustomsBrokerVUCreateDTO, db: Session = Depends(get_core_db)):
@router.put(
"/customs-broker-vu/{broker_key}", response_model=dto.CustomsBrokerVUCreateDTO
)
def update_customs_broker_vu(
broker_key: str,
vu_data: dto.CustomsBrokerVUCreateDTO,
db: Session = Depends(get_core_db),
):
updated_vu = services.CustomsBrokerVUService.update_vu(db, broker_key, vu_data)
if not updated_vu:
raise HTTPException(status_code=404, detail="Customs Broker VU not found")
return updated_vu
@router.put("/customs-broker-personnel/{broker_key}/{line}", response_model=dto.CustomsBrokerPersonnelDTO)
def update_customs_broker_personnel(broker_key: str, line: int, personnel_data: dto.CustomsBrokerPersonnelDTO, db: Session = Depends(get_core_db)):
updated_personnel = services.CustomsBrokerPersonnelService.update_personnel(db, broker_key, line, personnel_data)
@router.put(
"/customs-broker-personnel/{broker_key}/{line}",
response_model=dto.CustomsBrokerPersonnelDTO,
)
def update_customs_broker_personnel(
broker_key: str,
line: int,
personnel_data: dto.CustomsBrokerPersonnelDTO,
db: Session = Depends(get_core_db),
):
updated_personnel = services.CustomsBrokerPersonnelService.update_personnel(
db, broker_key, line, personnel_data
)
if not updated_personnel:
raise HTTPException(status_code=404, detail="Customs Broker Personnel not found")
raise HTTPException(
status_code=404, detail="Customs Broker Personnel not found"
)
return updated_personnel

View File

@@ -1,10 +1,16 @@
from sqlalchemy.orm import Session
from . import models, dto
from . import dto, models
class CustomsBrokerService:
@staticmethod
def get_by_broker_key(db: Session, broker_key: str):
return db.query(models.CustomsBroker).filter(models.CustomsBroker.broker_key == broker_key).first()
return (
db.query(models.CustomsBroker)
.filter(models.CustomsBroker.broker_key == broker_key)
.first()
)
@staticmethod
def create_customs_broker(db: Session, broker_data: dto.CustomsBrokerDTO):
@@ -26,7 +32,11 @@ class CustomsBrokerService:
class CustomsBrokerVUService:
@staticmethod
def get_by_broker_key(db: Session, broker_key: str):
return db.query(models.CustomsBrokerVU).filter(models.CustomsBrokerVU.broker_key == broker_key).first()
return (
db.query(models.CustomsBrokerVU)
.filter(models.CustomsBrokerVU.broker_key == broker_key)
.first()
)
@staticmethod
def create_vu(db: Session, vu_data: dto.CustomsBrokerVUCreateDTO):
@@ -58,10 +68,14 @@ class CustomsBrokerVUService:
class CustomsBrokerPersonnelService:
@staticmethod
def get_by_broker_key_and_line(db: Session, broker_key: str, line: int):
return db.query(models.CustomsBrokerPersonnel).filter(
models.CustomsBrokerPersonnel.broker_key == broker_key,
models.CustomsBrokerPersonnel.line == line
).first()
return (
db.query(models.CustomsBrokerPersonnel)
.filter(
models.CustomsBrokerPersonnel.broker_key == broker_key,
models.CustomsBrokerPersonnel.line == line,
)
.first()
)
@staticmethod
def create_personnel(db: Session, personnel_data: dto.CustomsBrokerPersonnelDTO):
@@ -72,8 +86,15 @@ class CustomsBrokerPersonnelService:
return new_personnel
@staticmethod
def update_personnel(db: Session, broker_key: str, line: int, personnel_data: dto.CustomsBrokerPersonnelDTO):
personnel = CustomsBrokerPersonnelService.get_by_broker_key_and_line(db, broker_key, line)
def update_personnel(
db: Session,
broker_key: str,
line: int,
personnel_data: dto.CustomsBrokerPersonnelDTO,
):
personnel = CustomsBrokerPersonnelService.get_by_broker_key_and_line(
db, broker_key, line
)
if personnel:
for key, value in personnel_data.dict(exclude_unset=True).items():
setattr(personnel, key, value)
@@ -83,7 +104,9 @@ class CustomsBrokerPersonnelService:
@staticmethod
def delete_personnel(db: Session, broker_key: str, line: int):
personnel = CustomsBrokerPersonnelService.get_by_broker_key_and_line(db, broker_key, line)
personnel = CustomsBrokerPersonnelService.get_by_broker_key_and_line(
db, broker_key, line
)
if personnel:
db.delete(personnel)
db.commit()

View File

@@ -2,9 +2,10 @@
DTOs para módulo de autenticación
"""
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
from pydantic import BaseModel, EmailStr, Field
class LoginRequestDTO(BaseModel):
"""DTO para solicitud de login"""

View File

@@ -2,22 +2,22 @@
Endpoints API para autenticación
"""
from fastapi import APIRouter, Depends, HTTPException, Response
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
from core.database import get_core_db
from core.security import get_current_user
from fastapi import APIRouter, Depends, HTTPException, Response
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from sqlalchemy.orm import Session
from .dto import (
ExchangeCodeRequestDTO,
LoginRequestDTO,
TokenResponseDTO,
RefreshTokenRequestDTO,
UserInfoResponseDTO,
LogoutRequestDTO,
RefreshTokenRequestDTO,
RegisterRequestDTO,
RegisterResponseDTO,
ExchangeCodeRequestDTO,
SetCookieRequestDTO,
TokenResponseDTO,
UserInfoResponseDTO,
)
from .service import AuthService

View File

@@ -2,21 +2,24 @@
Servicio de autenticación con Keycloak
"""
from keycloak import KeycloakOpenID, KeycloakAdmin
from keycloak.exceptions import KeycloakError
from fastapi import HTTPException
from sqlalchemy.orm import Session
import logging
from api.v1.modules.a76.tenants.service import TenantService
from api.v1.modules.a76.user_tenant.service import UserTenantService
from core.config import settings
from fastapi import HTTPException
from keycloak import KeycloakAdmin, KeycloakOpenID
from keycloak.exceptions import KeycloakError
from sqlalchemy.orm import Session
from .dto import (
LoginRequestDTO,
TokenResponseDTO,
RefreshTokenRequestDTO,
UserInfoResponseDTO,
LogoutRequestDTO,
RefreshTokenRequestDTO,
RegisterRequestDTO,
RegisterResponseDTO,
TokenResponseDTO,
UserInfoResponseDTO,
)
logger = logging.getLogger(__name__)
@@ -49,9 +52,6 @@ class AuthService:
"""
try:
# Verificar que el tenant existe
from api.v1.modules.a76.tenants.service import TenantService
from api.v1.modules.a76.user_tenant.service import UserTenantService
tenant_service = TenantService(self.db)
user_tenant_service = UserTenantService(self.db)
tenant = tenant_service.get_tenant_by_slug(login_data.tenant_slug)
@@ -70,54 +70,51 @@ class AuthService:
client_secret_key=settings.KEYCLOAK_CLIENT_SECRET,
)
# Obtener token de Keycloak
token_response = keycloak_client.token(
username=login_data.username,
password=login_data.password,
grant_type=["password"],
)
# PASO 1: Primero actualizamos los atributos del usuario ANTES de autenticar
# Esto es necesario para que los Protocol Mappers incluyan los valores correctos
# en el token que se generará a continuación
# Obtener información del usuario y verificar acceso al tenant
user_info = keycloak_client.userinfo(token_response["access_token"])
user_id = user_info.get("sub")
if user_id:
# Verificar si el usuario tiene acceso a este tenant
has_access = user_tenant_service.user_has_access_to_tenant(
user_id, tenant.id
# Para obtener el user_id, necesitamos hacer una autenticación temporal
# o buscar el usuario por username
try:
keycloak_admin = KeycloakAdmin(
server_url=settings.KEYCLOAK_SERVER_URL,
username=settings.KEYCLOAK_ADMIN_USERNAME,
password=settings.KEYCLOAK_ADMIN_PASSWORD,
realm_name=tenant.keycloak_realm,
user_realm_name="master",
verify=True,
)
if not has_access:
logger.warning(
f"User {user_id} tried to access tenant {tenant.id} without permission"
)
raise HTTPException(
status_code=403, detail="You don't have access to this tenant"
# Buscar usuario por username
users = keycloak_admin.get_users({"username": login_data.username})
if users and len(users) > 0:
user_id = users[0]["id"]
# Verificar si el usuario tiene acceso a este tenant
has_access = user_tenant_service.user_has_access_to_tenant(
user_id, tenant.id
)
# Actualizar el tenant_id del usuario en Keycloak basado en el slug usado
try:
# Crear instancia de KeycloakAdmin para actualizar atributos
keycloak_admin = KeycloakAdmin(
server_url=settings.KEYCLOAK_SERVER_URL,
username=settings.KEYCLOAK_ADMIN_USERNAME,
password=settings.KEYCLOAK_ADMIN_PASSWORD,
realm_name=tenant.keycloak_realm,
user_realm_name="master",
verify=True,
)
if not has_access:
logger.warning(
f"User {user_id} tried to access tenant {tenant.id} without permission"
)
raise HTTPException(
status_code=403,
detail="You don't have access to this tenant",
)
# Obtener los datos actuales del usuario para no sobrescribirlos
# Obtener los datos actuales del usuario
current_user = keycloak_admin.get_user(user_id)
# Obtener los atributos actuales o crear un dict vacío
current_attributes = current_user.get("attributes", {})
# Actualizar solo los atributos de tenant
# Actualizar los atributos de tenant
current_attributes["tenant_id"] = [str(tenant.id)]
current_attributes["tenant_slug"] = [tenant.slug]
# Actualizar el usuario enviando TODOS los campos para evitar que se borren
# Actualizar el usuario con los nuevos atributos
update_payload = {
"email": current_user.get("email"),
"firstName": current_user.get("firstName"),
@@ -128,13 +125,23 @@ class AuthService:
}
keycloak_admin.update_user(user_id=user_id, payload=update_payload)
logger.info(
f"Updated tenant_id={tenant.id} for user {login_data.username}"
)
except Exception as e:
# No queremos que falle el login si no se puede actualizar el atributo
logger.warning(f"Could not update tenant_id attribute: {str(e)}")
except KeycloakError as e:
logger.warning(f"Could not pre-update user attributes: {str(e)}")
# Continuamos con el login aunque falle la actualización
except HTTPException:
raise # Re-lanzamos las excepciones HTTP (como acceso denegado)
except Exception as e:
logger.warning(f"Error pre-updating user attributes: {str(e)}")
# PASO 2: Ahora autenticamos al usuario
# Si los Protocol Mappers están configurados, el token incluirá
# automáticamente los atributos tenant_id y tenant_slug actualizados
token_response = keycloak_client.token(
username=login_data.username,
password=login_data.password,
grant_type=["password"],
)
return TokenResponseDTO(
access_token=token_response["access_token"],
@@ -234,7 +241,6 @@ class AuthService:
"""
try:
self.keycloak_openid.logout(logout_data.refresh_token)
logger.info("User logged out successfully")
return {"message": "Logged out successfully"}
except KeycloakError as e:
@@ -307,7 +313,6 @@ class AuthService:
user_role = keycloak_admin.get_realm_role("user")
if user_role:
keycloak_admin.assign_realm_roles(user_id, [user_role])
logger.info(f"Assigned 'user' role to {register_data.username}")
except KeycloakError as e:
# El rol 'user' no existe, no es un error crítico
logger.warning(f"Could not assign 'user' role: {str(e)}")
@@ -322,23 +327,17 @@ class AuthService:
tenant_id=tenant.id,
role="user", # Rol por defecto
)
logger.info(f"Added user {user_id} to tenant {tenant.id} in database")
except Exception as e:
# Si falla, hacer rollback del usuario en Keycloak
logger.error(f"Failed to add user to tenant in database: {str(e)}")
try:
keycloak_admin.delete_user(user_id)
logger.info(f"Rolled back user creation in Keycloak")
except:
except Exception as e:
pass
raise HTTPException(
status_code=500, detail="Failed to register user in database"
)
logger.info(
f"User registered: {register_data.username} (tenant: {tenant.slug}, user_id: {user_id})"
)
return RegisterResponseDTO(
user_id=user_id,
username=register_data.username,
@@ -385,7 +384,6 @@ class AuthService:
"""
try:
# Importar el DTO aquí para evitar referencias circulares
from .dto import ExchangeCodeRequestDTO
# Intercambiar código por tokens usando Keycloak
token_response = self.keycloak_openid.token(
@@ -394,20 +392,11 @@ class AuthService:
redirect_uri=exchange_data.redirect_uri,
)
logger.info(f"Code exchanged successfully")
# Si se proporciona tenant_slug, podríamos validar que el usuario pertenece a ese tenant
# Por ahora simplemente retornamos los tokens
if exchange_data.tenant_slug:
# Decodificar token para obtener tenant_id del usuario
user_info = self.keycloak_openid.introspect(
token_response["access_token"]
)
user_tenant_id = user_info.get("tenant_id")
# Validar que el tenant existe y está activo
from api.v1.modules.a76.tenants.service import TenantService
tenant_service = TenantService(self.db)
tenant = tenant_service.get_tenant_by_slug(exchange_data.tenant_slug)

View File

@@ -3,9 +3,9 @@ DTOs (Data Transfer Objects) para módulo de clases SCAII y SCAF
Reemplaza schemas.py siguiendo enfoque DDD y estilo NestJS
"""
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
from pydantic import BaseModel, Field
class ClassCreateDTO(BaseModel):

View File

@@ -3,24 +3,26 @@ Modelos ORM para gestión de clases SCAII y SCAF
"""
from typing import TYPE_CHECKING, Optional
from api.v1.common.base_models import TenantScopedMixin
from core.database import Base
from sqlalchemy import (
Integer,
String,
SmallInteger,
ForeignKey,
PrimaryKeyConstraint,
ForeignKeyConstraint,
Integer,
PrimaryKeyConstraint,
SmallInteger,
String,
UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from core.database import Base
if TYPE_CHECKING:
from api.v1.modules.a76.parts.models import Part
from api.v1.modules.public.reference_data.material_types.models import MaterialType
class Class(Base):
class Class(Base, TenantScopedMixin):
"""
Modelo para la tabla GClases - Información de clases en sistemas SCAII y SCAF
"""
@@ -52,8 +54,6 @@ class Class(Base):
)
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)
client_id: Mapped[int] = mapped_column(Integer)
# Unique constraint compuesta

View File

@@ -2,21 +2,22 @@
Endpoints API para gestión de clases SCAII y SCAF
"""
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from typing import List, Optional
from core.database import get_core_db
from core.security import get_current_user, has_role
from .service import ClassService
from core.security import get_current_user
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from .dto import (
ClassCreateDTO,
ClassUpdateDTO,
ClassResponseDTO,
ClassBasicDTO,
ClassCreateDTO,
ClassListDTO,
ClassResponseDTO,
ClassSearchDTO,
ClassUpdateDTO,
)
from .service import ClassService
router = APIRouter(prefix="/classes", tags=["Classes"])
@@ -46,7 +47,9 @@ async def list_classes(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = ClassService(db)
search_params = ClassSearchDTO(
@@ -76,7 +79,9 @@ async def get_classes_by_client(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = ClassService(db)
return service.search_by_client(client_id, skip, limit)

View File

@@ -2,22 +2,23 @@
Capa de servicio para lógica de negocio de clases SCAII y SCAF
"""
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from sqlalchemy import or_, and_, func
from fastapi import HTTPException
from typing import List, Optional
import logging
from typing import List, Optional
from fastapi import HTTPException
from sqlalchemy import and_, or_
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from .models import Class
from .dto import (
ClassCreateDTO,
ClassUpdateDTO,
ClassResponseDTO,
ClassBasicDTO,
ClassCreateDTO,
ClassListDTO,
ClassResponseDTO,
ClassSearchDTO,
ClassUpdateDTO,
)
from .models import Class
logger = logging.getLogger(__name__)
@@ -79,8 +80,6 @@ class ClassService:
self.db.commit()
self.db.refresh(db_class)
logger.info(f"Class created: {db_class.client_id}-{db_class.class_code}")
return ClassResponseDTO.model_validate(db_class)
except IntegrityError as e:
@@ -218,7 +217,6 @@ class ClassService:
self.db.commit()
self.db.refresh(class_obj)
logger.info(f"Class updated: {client_id}-{class_code}")
return ClassResponseDTO.model_validate(class_obj)
@@ -250,7 +248,6 @@ class ClassService:
try:
self.db.delete(class_obj)
self.db.commit()
logger.info(f"Class deleted: {client_id}-{class_code}")
return True
except Exception as e:
self.db.rollback()

View File

@@ -1,12 +1,14 @@
import pytest
from fastapi.testclient import TestClient
from .routes import router
from fastapi import FastAPI
from fastapi.testclient import TestClient
from .routes import router
app = FastAPI()
app.include_router(router)
client = TestClient(app)
@pytest.mark.usefixtures("client", "access_token")
def test_list_classes(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
@@ -16,16 +18,19 @@ def test_list_classes(client, access_token):
assert "page" in response.json()
assert "page_size" in response.json()
@pytest.mark.usefixtures("client", "access_token")
def test_get_class_not_found(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/classes/invalid_id", headers=headers)
assert response.status_code == 404
def test_create_class_forbidden():
response = client.post("/classes/", json={"name": "Test Class"})
assert response.status_code in (403, 405, 404)
def test_update_class_forbidden():
response = client.put("/classes/1", json={"name": "Updated Class"})
assert response.status_code in (403, 405, 404)

View File

@@ -3,10 +3,10 @@ DTOs (Data Transfer Objects) para módulo de clientes y proveedores
Reemplaza schemas.py siguiendo enfoque DDD y estilo NestJS
"""
from pydantic import BaseModel, Field, EmailStr
from typing import Optional
from datetime import datetime
from decimal import Decimal
from typing import Optional
from pydantic import BaseModel, Field
# DTOs para dirección

View File

@@ -2,22 +2,24 @@
Modelos ORM para gestión de clientes y proveedores
"""
from typing import Optional
from decimal import Decimal
from typing import Optional
from api.v1.common.base_models import TenantScopedMixin
from core.database import Base
from sqlalchemy import (
Integer,
String,
SmallInteger,
Numeric,
ForeignKey,
PrimaryKeyConstraint,
ForeignKeyConstraint,
Integer,
Numeric,
PrimaryKeyConstraint,
SmallInteger,
String,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from core.database import Base
class ClientProvider(Base):
class ClientProvider(Base, TenantScopedMixin):
"""
Modelo para la tabla GClientesPro - Información de clientes y proveedores
"""
@@ -36,8 +38,6 @@ class ClientProvider(Base):
# 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(
@@ -67,7 +67,7 @@ class ClientProvider(Base):
)
class ClientProviderAddress(Base):
class ClientProviderAddress(Base, TenantScopedMixin):
"""
Modelo para la tabla GClientesPro_Direccion - Dirección de clientes y proveedores
"""
@@ -93,8 +93,6 @@ class ClientProviderAddress(Base):
Integer, ForeignKey("a76.client_provider.id", ondelete="CASCADE")
)
tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
# Address information
municipality: Mapped[Optional[str]] = mapped_column(String(150))
streets: Mapped[Optional[str]] = mapped_column(String(100))
@@ -115,7 +113,7 @@ class ClientProviderAddress(Base):
client_provider: Mapped["ClientProvider"] = relationship(back_populates="address")
class ClientProviderPrograms(Base):
class ClientProviderPrograms(Base, TenantScopedMixin):
"""
Modelo para la tabla GClientesPro_Programas - Programas de clientes y proveedores
"""
@@ -141,8 +139,6 @@ class ClientProviderPrograms(Base):
Integer, ForeignKey("a76.client_provider.id", ondelete="CASCADE")
)
tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
# Program information
program: Mapped[Optional[str]] = mapped_column(String(7))
program_number: Mapped[Optional[str]] = mapped_column(String(40))

View File

@@ -2,20 +2,21 @@
Endpoints API para gestión de clientes y proveedores
"""
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from typing import List, Optional
from core.database import get_core_db
from core.security import get_current_user, has_role
from .service import ClientProviderService
from core.security import get_current_user
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from .dto import (
ClientProviderCreateDTO,
ClientProviderUpdateDTO,
ClientProviderResponseDTO,
ClientProviderBasicDTO,
ClientProviderCreateDTO,
ClientProviderListDTO,
ClientProviderResponseDTO,
ClientProviderUpdateDTO,
)
from .service import ClientProviderService
router = APIRouter(prefix="/clients-providers")
@@ -36,11 +37,15 @@ async def create_client_provider(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
# Ensure the client_data is associated with the correct tenant and company
if client_data.tenant_id != tenant_id or client_data.company_id != company_id:
raise HTTPException(status_code=400, detail="Mismatch in tenant or company association")
raise HTTPException(
status_code=400, detail="Mismatch in tenant or company association"
)
service = ClientProviderService(db)
return service.create_client_provider(client_data)
@@ -68,7 +73,9 @@ async def list_clients_providers(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = ClientProviderService(db)
return service.list_clients_providers(
@@ -91,7 +98,9 @@ async def get_clients_only(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = ClientProviderService(db)
return service.get_clients_only(skip, limit)
@@ -112,7 +121,9 @@ async def get_providers_only(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = ClientProviderService(db)
return service.get_providers_only(skip, limit)
@@ -132,7 +143,9 @@ async def search_by_rfc(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = ClientProviderService(db)
return service.search_by_rfc(rfc)
@@ -152,7 +165,9 @@ async def get_client_provider(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = ClientProviderService(db)
client = service.get_client_provider(client_id)
@@ -178,7 +193,9 @@ async def update_client_provider(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = ClientProviderService(db)
client = service.update_client_provider(client_id, client_data)
@@ -205,7 +222,9 @@ async def delete_client_provider(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = ClientProviderService(db)
if not service.delete_client_provider(client_id):
@@ -228,7 +247,9 @@ async def toggle_client_provider_status(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = ClientProviderService(db)
client = service.toggle_status(client_id)
@@ -254,7 +275,9 @@ async def get_client_provider_address(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = ClientProviderService(db)
client = service.get_client_provider(client_id)
@@ -280,7 +303,9 @@ async def get_client_provider_programs(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = ClientProviderService(db)
client = service.get_client_provider(client_id)
@@ -306,7 +331,9 @@ async def get_client_provider_basic_info(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = ClientProviderService(db)
client = service.get_client_provider(client_id)

View File

@@ -2,23 +2,22 @@
Capa de servicio para lógica de negocio de clientes y proveedores
"""
from sqlalchemy.orm import Session, joinedload
from sqlalchemy.exc import IntegrityError
from sqlalchemy import or_, and_
from fastapi import HTTPException
from typing import List, Optional
import logging
from typing import List, Optional
from fastapi import HTTPException
from sqlalchemy import or_
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session, joinedload
from .models import ClientProvider, ClientProviderAddress, ClientProviderPrograms
from .dto import (
ClientProviderCreateDTO,
ClientProviderUpdateDTO,
ClientProviderResponseDTO,
ClientProviderBasicDTO,
ClientProviderCreateDTO,
ClientProviderListDTO,
ClientProviderAddressDTO,
ClientProviderProgramsDTO,
ClientProviderResponseDTO,
ClientProviderUpdateDTO,
)
from .models import ClientProvider, ClientProviderAddress, ClientProviderPrograms
logger = logging.getLogger(__name__)
@@ -99,10 +98,6 @@ class ClientProviderService:
self.db.commit()
self.db.refresh(db_client)
logger.info(
f"Client/Provider created: {db_client.client_id} - {db_client.name}"
)
return self._get_client_with_relations(client_data.client_id)
except IntegrityError as e:
@@ -282,7 +277,6 @@ class ClientProviderService:
self.db.add(programs)
self.db.commit()
logger.info(f"Client/Provider updated: {client_id}")
return self._get_client_with_relations(client_id)
@@ -314,7 +308,6 @@ class ClientProviderService:
try:
self.db.delete(client) # Las relaciones se eliminan en cascada
self.db.commit()
logger.info(f"Client/Provider deleted: {client_id}")
return True
except Exception as e:
self.db.rollback()

View File

@@ -1,12 +1,14 @@
import pytest
from fastapi.testclient import TestClient
from .routes import router
from fastapi import FastAPI
from fastapi.testclient import TestClient
from .routes import router
app = FastAPI()
app.include_router(router)
client = TestClient(app)
@pytest.mark.usefixtures("client", "access_token")
def test_list_clients_and_providers(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
@@ -16,16 +18,23 @@ def test_list_clients_and_providers(client, access_token):
assert "page" in response.json()
assert "page_size" in response.json()
@pytest.mark.usefixtures("client", "access_token")
def test_get_client_or_provider_not_found(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/client_and_provider/invalid_id", headers=headers)
assert response.status_code == 404
def test_create_client_or_provider_forbidden():
response = client.post("/client_and_provider/", json={"name": "Test Client/Provider"})
response = client.post(
"/client_and_provider/", json={"name": "Test Client/Provider"}
)
assert response.status_code in (403, 405, 404)
def test_update_client_or_provider_forbidden():
response = client.put("/client_and_provider/1", json={"name": "Updated Client/Provider"})
response = client.put(
"/client_and_provider/1", json={"name": "Updated Client/Provider"}
)
assert response.status_code in (403, 405, 404)

View File

@@ -3,9 +3,10 @@ DTOs (Data Transfer Objects) para módulo de empresa
Reemplaza schemas.py siguiendo enfoque DDD y estilo NestJS
"""
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, Field
class CompanyCreateDTO(BaseModel):

View File

@@ -3,24 +3,21 @@ 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,
UniqueConstraint,
)
from sqlalchemy.sql import func
from sqlalchemy.orm import Mapped, mapped_column
from api.v1.common.base_models import TimestampMixin
from core.database import Base
from sqlalchemy import (
Boolean,
ForeignKeyConstraint,
Integer,
PrimaryKeyConstraint,
SmallInteger,
String,
)
from sqlalchemy.orm import Mapped, mapped_column
class Company(Base):
class Company(Base, TimestampMixin):
"""
Modelo para la tabla Company - Información de la empresa
"""
@@ -81,12 +78,3 @@ class Company(Base):
seventh_amendment: Mapped[Optional[bool]] = mapped_column(
Boolean
) # FINALCONTADORAELECTRONICO renombrado
# 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

@@ -2,14 +2,15 @@
Endpoints API para gestión de empresa
"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import Optional
from core.database import get_core_db
from core.security import get_current_user, has_role, get_tenant_from_token
from core.security import get_current_user, get_tenant_from_token
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from .dto import CompanyCreateDTO, CompanyResponseDTO, CompanyUpdateDTO
from .service import CompanyService
from .dto import CompanyCreateDTO, CompanyUpdateDTO, CompanyResponseDTO
router = APIRouter(prefix="/company")
@@ -49,8 +50,7 @@ async def get_company(
@router.get("/my-companies", response_model=list[CompanyResponseDTO])
async def get_my_companies(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)
):
"""
Get all companies that belong to the user's tenant
@@ -59,10 +59,7 @@ async def get_my_companies(
"""
tenant_id = get_tenant_from_token(current_user)
if not tenant_id:
raise HTTPException(
status_code=400,
detail="Tenant ID not found in token"
)
raise HTTPException(status_code=400, detail="Tenant ID not found in token")
service = CompanyService(db)
companies = service.get_companies_by_tenant(tenant_id)
@@ -202,4 +199,3 @@ async def delete_company(
raise HTTPException(
status_code=404, detail=f"Company with ID '{company_id}' not found"
)

View File

@@ -2,14 +2,15 @@
Capa de servicio para lógica de negocio de empresa
"""
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from fastapi import HTTPException
from typing import List, Optional
import logging
from typing import List, Optional
from fastapi import HTTPException
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from .dto import CompanyCreateDTO, CompanyResponseDTO, CompanyUpdateDTO
from .models import Company
from .dto import CompanyCreateDTO, CompanyUpdateDTO, CompanyResponseDTO
logger = logging.getLogger(__name__)
@@ -35,9 +36,7 @@ class CompanyService:
"""
try:
# Verificar que no exista ya una empresa (solo puede haber una por el consecutivo único)
existing = (
self.db.query(Company).filter(Company.consecutive == True).first()
)
existing = self.db.query(Company).filter(Company.id).first()
if existing:
raise HTTPException(
status_code=400,
@@ -82,8 +81,6 @@ class CompanyService:
self.db.commit()
self.db.refresh(db_company)
logger.info(f"Company created: {db_company.id} - {db_company.name}")
return CompanyResponseDTO.model_validate(db_company)
except IntegrityError as e:
@@ -107,7 +104,7 @@ class CompanyService:
Returns:
CompanyResponseDTO o None si no existe
"""
company = self.db.query(Company).filter(Company.consecutive == True).first()
company = self.db.query(Company).filter(Company.id).first()
if not company:
return None
return CompanyResponseDTO.model_validate(company)
@@ -152,7 +149,6 @@ class CompanyService:
try:
self.db.commit()
self.db.refresh(company)
logger.info(f"Company updated: {company_id}")
return CompanyResponseDTO.model_validate(company)
except Exception as e:
self.db.rollback()
@@ -176,7 +172,6 @@ class CompanyService:
try:
self.db.delete(company)
self.db.commit()
logger.info(f"Company deleted: {company_id}")
return True
except Exception as e:
self.db.rollback()
@@ -190,10 +185,7 @@ class CompanyService:
Returns:
True si existe una empresa, False en caso contrario
"""
return (
self.db.query(Company).filter(Company.consecutive == True).first()
is not None
)
return self.db.query(Company).filter(Company.id).first() is not None
def get_companies_by_tenant(self, tenant_id: int) -> List[CompanyResponseDTO]:
"""

View File

@@ -1,12 +1,14 @@
import pytest
from fastapi.testclient import TestClient
from .routes import router
from fastapi import FastAPI
from fastapi.testclient import TestClient
from .routes import router
app = FastAPI()
app.include_router(router)
client = TestClient(app)
@pytest.mark.usefixtures("client", "access_token")
def test_list_companies(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
@@ -16,16 +18,19 @@ def test_list_companies(client, access_token):
assert "page" in response.json()
assert "page_size" in response.json()
@pytest.mark.usefixtures("client", "access_token")
def test_get_company_not_found(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/company/invalid_id", headers=headers)
assert response.status_code == 404
def test_create_company_forbidden():
response = client.post("/company/", json={"name": "Test Company"})
assert response.status_code in (403, 405, 404)
def test_update_company_forbidden():
response = client.put("/company/1", json={"name": "Updated Company"})
assert response.status_code in (403, 405, 404)

View File

@@ -1,16 +1,16 @@
from api.v1.common.base_models import TenantScopedMixin
from core.database import Base
from sqlalchemy import (
Integer,
String,
ForeignKey,
PrimaryKeyConstraint,
ForeignKeyConstraint,
Integer,
PrimaryKeyConstraint,
String,
UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column
from core.database import Base
class CountryRuleOct(Base):
class CountryRuleOct(Base, TenantScopedMixin):
__tablename__ = "country_rule_oct"
__table_args__ = (
PrimaryKeyConstraint("id", name="country_rule_oct_pkey"),
@@ -45,8 +45,6 @@ class CountryRuleOct(Base):
)
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()

View File

@@ -1,9 +1,10 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from core.database import get_core_db
from core.security import get_current_user
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from .dto import CountryRuleOctCreateDTO, CountryRuleOctResponseDTO
from .services import CountryRuleOctService
@@ -22,7 +23,9 @@ async def list_countries(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
return db.query(CountryRuleOctService).all()
@@ -47,9 +50,13 @@ async def read_country_rule(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
country = CountryRuleOctService.get_country_by_keys(db, permission, line, fraction, country_code)
country = CountryRuleOctService.get_country_by_keys(
db, permission, line, fraction, country_code
)
if not country:
raise HTTPException(status_code=404, detail="CountryRuleOct not found")
return country

View File

@@ -3,7 +3,8 @@ Service layer for CountryRuleOct.
"""
from sqlalchemy.orm import Session
from . import models, dto
from . import dto, models
class CountryRuleOctService:

View File

@@ -1,12 +1,14 @@
import pytest
from fastapi.testclient import TestClient
from .routes import router
from fastapi import FastAPI
from fastapi.testclient import TestClient
from .routes import router
app = FastAPI()
app.include_router(router)
client = TestClient(app)
@pytest.mark.usefixtures("client", "access_token")
def test_list_country_rules(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
@@ -16,16 +18,19 @@ def test_list_country_rules(client, access_token):
assert "page" in response.json()
assert "page_size" in response.json()
@pytest.mark.usefixtures("client", "access_token")
def test_get_country_rule_not_found(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/country-rule-oct/invalid_id", headers=headers)
assert response.status_code == 404
def test_create_country_rule_forbidden():
response = client.post("/country-rule-oct/", json={"rule": "Test Rule"})
assert response.status_code in (403, 405, 404)
def test_update_country_rule_forbidden():
response = client.put("/country-rule-oct/1", json={"rule": "Updated Rule"})
assert response.status_code in (403, 405, 404)

View File

@@ -1,6 +1,8 @@
from pydantic import BaseModel
from typing import Optional
from pydantic import BaseModel
class DriverBaseDTO(BaseModel):
transporter_key: str
line: int
@@ -29,9 +31,11 @@ class DriverBaseDTO(BaseModel):
company_id: str
tenant_id: str
class DriverCreateDTO(DriverBaseDTO):
pass
class DriverResponseDTO(DriverBaseDTO):
class Config:
from_attributes = True

View File

@@ -1,11 +1,22 @@
from sqlalchemy import Column, String, Integer, ForeignKey
from api.v1.common.base_models import TenantScopedMixin
from core.database import Base
from sqlalchemy import Column, ForeignKey, ForeignKeyConstraint, Integer, String
class Driver(Base):
class Driver(Base, TenantScopedMixin):
__tablename__ = "driver"
__table_args__ = {"schema": "a76"}
__table_args__ = (
ForeignKeyConstraint(["company_id"], ["a76.company.id"]),
ForeignKeyConstraint(["tenant_id"], ["a76.tenants.id"]),
{"schema": "a76"},
)
transporter_key = Column(String(5), ForeignKey("a76.transporter.transporter_key", ondelete="CASCADE"), primary_key=True, nullable=False)
transporter_key = Column(
String(5),
ForeignKey("a76.transporter.transporter_key", ondelete="CASCADE"),
primary_key=True,
nullable=False,
)
line = Column(Integer, primary_key=True, nullable=False)
driver_name = Column(String(80), nullable=True)
license_number = Column(String(29), nullable=True)
@@ -29,5 +40,3 @@ class Driver(Base):
badge_number = Column(String(20), nullable=True)
class_type = Column(String(1), nullable=True)
unique_badge_number = Column(String(100), nullable=True)
company_id = Column(Integer, ForeignKey("a76.company.id"), nullable=False)
tenant_id = Column(Integer, ForeignKey("a76.tenants.id"), nullable=False)

View File

@@ -1,47 +1,51 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from core.database import get_core_db
from core.security import get_current_user
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from .dto import DriverCreateDTO, DriverResponseDTO
from .services import DriverService
router = APIRouter(prefix="/drivers", tags=["Drivers"])
@router.get("/", response_model=List[DriverResponseDTO])
async def list_drivers(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)
):
return db.query(DriverService).all()
@router.get("/{transporter_key}/{line}", response_model=DriverResponseDTO)
async def read_driver(
transporter_key: str,
line: int,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
current_user: dict = Depends(get_current_user),
):
driver = DriverService.get_driver_by_key_and_line(db, transporter_key, line)
if not driver:
raise HTTPException(status_code=404, detail="Driver not found")
return driver
@router.post("/", response_model=DriverResponseDTO, status_code=status.HTTP_201_CREATED)
async def create_driver(
driver_data: DriverCreateDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
current_user: dict = Depends(get_current_user),
):
return DriverService.create_driver(db, driver_data)
@router.delete("/{transporter_key}/{line}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_driver(
transporter_key: str,
line: int,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
current_user: dict = Depends(get_current_user),
):
driver = DriverService.delete_driver(db, transporter_key, line)
if not driver:

View File

@@ -1,13 +1,19 @@
from sqlalchemy.orm import Session
from . import models, dto
from . import dto, models
class DriverService:
@staticmethod
def get_driver_by_key_and_line(db: Session, transporter_key: str, line: int):
return db.query(models.Driver).filter(
models.Driver.transporter_key == transporter_key,
models.Driver.line == line
).first()
return (
db.query(models.Driver)
.filter(
models.Driver.transporter_key == transporter_key,
models.Driver.line == line,
)
.first()
)
@staticmethod
def create_driver(db: Session, driver_data: dto.DriverCreateDTO):

View File

@@ -1,6 +1,7 @@
from pydantic import BaseModel
from typing import Optional
from pydantic import BaseModel
class ExchangeRateBaseDTO(BaseModel):
date: int

View File

@@ -1,20 +1,21 @@
from typing import Optional
from decimal import Decimal
from typing import Optional
from api.v1.common.base_models import TenantScopedMixin
from core.database import Base
from sqlalchemy import (
Integer,
String,
DECIMAL,
PrimaryKeyConstraint,
DateTime,
ForeignKeyConstraint,
Integer,
PrimaryKeyConstraint,
String,
UniqueConstraint,
ForeignKey,
)
from sqlalchemy.orm import Mapped, mapped_column
from core.database import Base
class ExchangeRate(Base):
class ExchangeRate(Base, TenantScopedMixin):
__tablename__ = "exchange_rate"
__table_args__ = (
PrimaryKeyConstraint("id", name="exchange_rate_pkey"),
@@ -31,8 +32,6 @@ class ExchangeRate(Base):
)
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))

View File

@@ -1,9 +1,10 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from core.database import get_core_db
from core.security import get_current_user
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from .dto import ExchangeRateCreateDTO, ExchangeRateResponseDTO
from .services import ExchangeRateService
@@ -22,7 +23,9 @@ async def list_exchange_rates(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
return db.query(ExchangeRateService).all()
@@ -41,7 +44,9 @@ async def read_exchange_rate(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
exchange_rate = ExchangeRateService.get_exchange_rate_by_date(db, date)
if not exchange_rate:
@@ -65,7 +70,9 @@ async def create_exchange_rate(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
return ExchangeRateService.create_exchange_rate(db, exchange_rate_data)
@@ -84,7 +91,9 @@ async def delete_exchange_rate(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
exchange_rate = ExchangeRateService.delete_exchange_rate(db, date)
if not exchange_rate:

View File

@@ -1,5 +1,6 @@
from sqlalchemy.orm import Session
from . import models, dto
from . import dto, models
class ExchangeRateService:

View File

@@ -1,12 +1,14 @@
import pytest
from fastapi.testclient import TestClient
from .routes import router
from fastapi import FastAPI
from fastapi.testclient import TestClient
from .routes import router
app = FastAPI()
app.include_router(router)
client = TestClient(app)
@pytest.mark.usefixtures("client", "access_token")
def test_list_exchange_rates(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
@@ -16,16 +18,19 @@ def test_list_exchange_rates(client, access_token):
assert "page" in response.json()
assert "page_size" in response.json()
@pytest.mark.usefixtures("client", "access_token")
def test_get_exchange_rate_not_found(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/exchange-rate/invalid_id", headers=headers)
assert response.status_code == 404
def test_create_exchange_rate_forbidden():
response = client.post("/exchange-rate/", json={"rate": 1.23})
assert response.status_code in (403, 405, 404)
def test_update_exchange_rate_forbidden():
response = client.put("/exchange-rate/1", json={"rate": 1.45})
assert response.status_code in (403, 405, 404)

View File

@@ -2,9 +2,10 @@
DTOs for FractionRuleOctave.
"""
from pydantic import BaseModel
from typing import Optional
from pydantic import BaseModel
class FractionRuleOctaveBaseDTO(BaseModel):
PERMISSION: str

View File

@@ -1,16 +1,16 @@
from api.v1.common.base_models import TenantScopedMixin
from core.database import Base
from sqlalchemy import (
Integer,
String,
ForeignKey,
PrimaryKeyConstraint,
ForeignKeyConstraint,
Integer,
PrimaryKeyConstraint,
String,
UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column
from core.database import Base
class FractionRuleOctave(Base):
class FractionRuleOctave(Base, TenantScopedMixin):
__tablename__ = "fraction_rule_octave"
__table_args__ = (
PrimaryKeyConstraint("id", name="fraction_rule_octave_pkey"),
@@ -32,8 +32,6 @@ class FractionRuleOctave(Base):
)
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)

View File

@@ -1,9 +1,10 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from core.database import get_core_db
from core.security import get_current_user
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from .dto import FractionRuleOctaveCreateDTO, FractionRuleOctaveResponseDTO
from .services import FractionRuleOctaveService
@@ -22,7 +23,9 @@ async def list_fractions(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
return db.query(FractionRuleOctaveService).all()
@@ -45,9 +48,13 @@ async def read_fraction(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
frac = FractionRuleOctaveService.get_fraction_by_permission_line(db, permission, line, fraction)
frac = FractionRuleOctaveService.get_fraction_by_permission_line(
db, permission, line, fraction
)
if not frac:
raise HTTPException(status_code=404, detail="FractionRuleOctave not found")
return frac
@@ -71,7 +78,9 @@ async def create_frac(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
return FractionRuleOctaveService.create_frac(db, frac_data)
@@ -94,7 +103,9 @@ async def delete_fraction(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
frac = FractionRuleOctaveService.delete_fraction(db, permission, line, fraction)
if not frac:

View File

@@ -1,5 +1,6 @@
from sqlalchemy.orm import Session
from . import models, dto
from . import dto, models
"""
Service layer for FractionRuleOctave.

View File

@@ -1,12 +1,14 @@
import pytest
from fastapi.testclient import TestClient
from .routes import router
from fastapi import FastAPI
from fastapi.testclient import TestClient
from .routes import router
app = FastAPI()
app.include_router(router)
client = TestClient(app)
@pytest.mark.usefixtures("client", "access_token")
def test_list_fraction_rules(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
@@ -16,16 +18,19 @@ def test_list_fraction_rules(client, access_token):
assert "page" in response.json()
assert "page_size" in response.json()
@pytest.mark.usefixtures("client", "access_token")
def test_get_fraction_rule_not_found(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/fraction_rule_octave/invalid_id", headers=headers)
assert response.status_code == 404
def test_create_fraction_rule_forbidden():
response = client.post("/fraction_rule_octave/", json={"rule": "Test Rule"})
assert response.status_code in (403, 405, 404)
def test_update_fraction_rule_forbidden():
response = client.put("/fraction_rule_octave/1", json={"rule": "Updated Rule"})
assert response.status_code in (403, 405, 404)

View File

@@ -2,10 +2,11 @@
DTOs para módulo de licencias
"""
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field
class LicensePlanDTO(str, Enum):

View File

@@ -2,22 +2,14 @@
Modelos ORM para gestión de licencias
"""
from datetime import datetime
from sqlalchemy import (
Column,
Integer,
String,
DateTime,
Boolean,
ForeignKey,
Enum as SQLEnum,
)
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from sqlalchemy.orm import Mapped, mapped_column
from core.database import Base
import enum
from api.v1.common.base_models import TimestampMixin
from core.database import Base
from sqlalchemy import Boolean, Column, DateTime
from sqlalchemy import Enum as SQLEnum
from sqlalchemy import ForeignKey, Integer
class LicensePlan(enum.Enum):
"""Planes de licencia disponibles"""
@@ -38,7 +30,7 @@ class LicenseStatus(enum.Enum):
CANCELLED = "cancelled"
class License(Base):
class License(Base, TimestampMixin):
"""
Modelo de Licencia - Control de planes y límites por tenant
"""
@@ -72,20 +64,11 @@ class License(Base):
starts_at = Column(DateTime(timezone=True), nullable=False)
expires_at = Column(DateTime(timezone=True), nullable=False)
# 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)
def __repr__(self):
return f"<License(tenant_id={self.tenant_id}, plan={self.plan.value}, status={self.status.value})>"
class LicenseUsage(Base):
class LicenseUsage(Base, TimestampMixin):
"""
Modelo para tracking de uso de licencia
"""
@@ -107,14 +90,5 @@ class LicenseUsage(Base):
operations_count = Column(Integer, default=0)
api_calls_count = Column(Integer, default=0)
# 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)
def __repr__(self):
return f"<LicenseUsage(tenant_id={self.tenant_id}, operations={self.operations_count})>"

View File

@@ -2,17 +2,17 @@
Endpoints API para gestión de licencias
"""
from core.database import get_core_db
from core.security import get_current_user, has_role
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from core.database import get_core_db
from core.security import get_current_user, has_role
from .dto import (
LicenseCreateDTO,
LicenseUpdateDTO,
LicenseResponseDTO,
LicenseValidationResponseDTO,
LicenseUpdateDTO,
LicenseUsageResponseDTO,
LicenseValidationResponseDTO,
)
from .service import LicenseService

View File

@@ -2,21 +2,21 @@
Servicio de lógica de negocio para licencias
"""
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from fastapi import HTTPException
from typing import Optional
from datetime import datetime, timezone
import logging
from datetime import datetime, timezone
from typing import Optional
from fastapi import HTTPException
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from .models import License, LicenseUsage, LicensePlan, LicenseStatus
from .dto import (
LicenseCreateDTO,
LicenseUpdateDTO,
LicenseResponseDTO,
LicenseValidationResponseDTO,
LicenseUpdateDTO,
LicenseUsageResponseDTO,
)
from .models import License, LicensePlan, LicenseStatus, LicenseUsage
logger = logging.getLogger(__name__)
@@ -74,8 +74,6 @@ class LicenseService:
self.db.commit()
self.db.refresh(db_license)
logger.info(f"License created for tenant {license_data.tenant_id}")
return LicenseResponseDTO.model_validate(db_license)
except IntegrityError as e:

View File

@@ -2,9 +2,10 @@
DTOs for GBultos.
"""
from pydantic import BaseModel
from typing import Optional
from pydantic import BaseModel
class GBultoBaseDTO(BaseModel):
CODE: str

View File

@@ -1,22 +1,20 @@
from typing import Optional
from datetime import datetime
from decimal import Decimal
from typing import Optional
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
DateTime,
Integer,
String,
DECIMAL,
ForeignKey,
PrimaryKeyConstraint,
ForeignKeyConstraint,
Integer,
PrimaryKeyConstraint,
String,
UniqueConstraint,
)
from sqlalchemy.sql import func
from sqlalchemy.orm import Mapped, mapped_column
from core.database import Base
class Package(Base):
class Package(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "packages" # GBultos
__table_args__ = (
PrimaryKeyConstraint("id", name="packages_pkey"),
@@ -31,8 +29,6 @@ class Package(Base):
)
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))
@@ -42,12 +38,3 @@ class Package(Base):
plural_in: Mapped[Optional[str]] = mapped_column(String(4))
code_ace: Mapped[Optional[str]] = mapped_column(String(4))
code_aamex: Mapped[Optional[str]] = mapped_column(String(9))
# 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,11 +1,12 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List
from core.database import get_core_db
from core.security import get_current_user
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from .dto import GBultoCreateDTO, GBultoResponseDTO, GBultoUpdateDTO
from .models import Package
from .dto import GBultoCreateDTO, GBultoUpdateDTO, GBultoResponseDTO
from .services import GBultoService
router = APIRouter(prefix="/bultos", tags=["GBultos"])
@@ -26,7 +27,9 @@ async def list_bultos(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
return db.query(Package).offset(skip).limit(limit).all()
@@ -45,7 +48,9 @@ async def read_bulto(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
bulto = GBultoService.get_bulto_by_code(db, code)
if not bulto:
@@ -67,7 +72,9 @@ async def create_gbulto(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
return GBultoService.create_gbulto(db, bulto_data)
@@ -87,7 +94,9 @@ async def update_bulto(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
bulto = GBultoService.update_bulto(db, code, bulto_data)
if not bulto:
@@ -109,7 +118,9 @@ async def delete_bulto(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
bulto = GBultoService.delete_bulto(db, code)
if not bulto:

View File

@@ -1,5 +1,6 @@
from sqlalchemy.orm import Session
from . import models, dto
from . import dto, models
class GBultoService:

View File

@@ -1,12 +1,14 @@
import pytest
from fastapi.testclient import TestClient
from .routes import router
from fastapi import FastAPI
from fastapi.testclient import TestClient
from .routes import router
app = FastAPI()
app.include_router(router)
client = TestClient(app)
@pytest.mark.usefixtures("client", "access_token")
def test_list_packages(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
@@ -16,16 +18,19 @@ def test_list_packages(client, access_token):
assert "page" in response.json()
assert "page_size" in response.json()
@pytest.mark.usefixtures("client", "access_token")
def test_get_package_not_found(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/bultos/invalid_id", headers=headers)
assert response.status_code == 404
def test_create_package_forbidden():
response = client.post("/bultos/", json={"name": "Test Package"})
assert response.status_code in (403, 405, 404)
def test_update_package_forbidden():
response = client.put("/bultos/1", json={"name": "Updated Package"})
assert response.status_code in (403, 405, 404)

View File

@@ -3,10 +3,11 @@ DTOs (Data Transfer Objects) para módulo de partes/componentes
Reemplaza schemas.py siguiendo enfoque DDD y estilo NestJS
"""
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
from decimal import Decimal
from typing import Optional
from pydantic import BaseModel, Field
class PartCreateDTO(BaseModel):

View File

@@ -2,30 +2,30 @@
Modelos ORM para gestión de partes/componentes
"""
from typing import TYPE_CHECKING, Optional
from datetime import datetime
from decimal import Decimal
from typing import TYPE_CHECKING, Optional
from api.v1.common.base_models import TenantScopedMixin
from core.database import Base
from sqlalchemy import (
Integer,
String,
Numeric,
SmallInteger,
ForeignKey,
PrimaryKeyConstraint,
ForeignKeyConstraint,
Integer,
Numeric,
PrimaryKeyConstraint,
SmallInteger,
String,
UniqueConstraint,
)
from sqlalchemy.sql import func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from core.database import Base
if TYPE_CHECKING:
from api.v1.modules.a76.classes.models import Class
from api.v1.modules.public.reference_data.countries.models import Country
from api.v1.modules.public.reference_data.currency_types.models import CurrencyType
from api.v1.modules.a76.classes.models import Class
class Part(Base):
class Part(Base, TenantScopedMixin):
"""
Modelo para la tabla GPartes - Información de partes en los sistemas SCAII (N), SCAF (S) Y WINSAAI (W)
"""
@@ -50,8 +50,6 @@ class Part(Base):
)
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)
# Unique constraint compuesta
client_id: Mapped[int] = mapped_column(Integer)

View File

@@ -2,21 +2,22 @@
Endpoints API para gestión de partes/componentes
"""
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from typing import List, Optional
from core.database import get_core_db
from core.security import get_current_user, has_role
from .service import PartService
from core.security import get_current_user
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from .dto import (
PartCreateDTO,
PartUpdateDTO,
PartResponseDTO,
PartBasicDTO,
PartCreateDTO,
PartListDTO,
PartResponseDTO,
PartSearchDTO,
PartUpdateDTO,
)
from .service import PartService
router = APIRouter(prefix="/parts")
@@ -35,7 +36,9 @@ async def create_part(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = PartService(db)
return service.create_part(part_data)
@@ -64,7 +67,9 @@ async def list_parts(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = PartService(db)
search_params = PartSearchDTO(
@@ -94,7 +99,9 @@ async def get_parts_by_client(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = PartService(db)
return service.search_by_client(client_id, skip, limit)
@@ -114,7 +121,9 @@ async def search_by_fraction(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = PartService(db)
return service.search_by_fraction(fraction)
@@ -134,7 +143,9 @@ async def search_by_supplier(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = PartService(db)
return service.search_by_supplier(supplier)
@@ -154,7 +165,9 @@ async def get_parts_by_country(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = PartService(db)
return service.get_parts_by_country(country_code)
@@ -172,7 +185,9 @@ async def get_parts_statistics(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = PartService(db)
return service.get_parts_statistics()
@@ -193,7 +208,9 @@ async def get_part(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = PartService(db)
part = service.get_part(client_id, part_number)
@@ -221,7 +238,9 @@ async def update_part(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = PartService(db)
part = service.update_part(client_id, part_number, part_data)
@@ -250,7 +269,9 @@ async def delete_part(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = PartService(db)
if not service.delete_part(client_id, part_number):
@@ -277,7 +298,9 @@ async def toggle_part_status(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = PartService(db)
part = service.toggle_status(client_id, part_number)
@@ -305,7 +328,9 @@ async def get_part_basic_info(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = PartService(db)
part = service.get_part(client_id, part_number)
@@ -342,7 +367,9 @@ async def get_part_regulatory_info(
company_id = current_user.get("company_id")
if not tenant_id or not company_id:
raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found")
raise HTTPException(
status_code=403, detail="Access denied: Tenant or Company not found"
)
service = PartService(db)
part = service.get_part(client_id, part_number)

View File

@@ -2,16 +2,16 @@
Capa de servicio para lógica de negocio de partes/componentes
"""
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from sqlalchemy import or_, and_, func
from fastapi import HTTPException
from typing import List, Optional
import logging
from datetime import datetime
from typing import List, Optional
from fastapi import HTTPException
from sqlalchemy import and_, func, or_
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from .models import Part
from .dto import PartCreateDTO, PartUpdateDTO
from .models import Part
logger = logging.getLogger(__name__)

View File

@@ -1,12 +1,14 @@
import pytest
from fastapi.testclient import TestClient
from .routes import router
from fastapi import FastAPI
from fastapi.testclient import TestClient
from .routes import router
app = FastAPI()
app.include_router(router)
client = TestClient(app)
@pytest.mark.usefixtures("client", "access_token")
def test_list_parts(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
@@ -16,16 +18,19 @@ def test_list_parts(client, access_token):
assert "page" in response.json()
assert "page_size" in response.json()
@pytest.mark.usefixtures("client", "access_token")
def test_get_part_not_found(client, access_token):
headers = {"Authorization": f"Bearer {access_token}"}
response = client.get("/parts/invalid_id", headers=headers)
assert response.status_code == 404
def test_create_part_forbidden():
response = client.post("/parts/", json={"name": "Test Part"})
assert response.status_code in (403, 405, 404)
def test_update_part_forbidden():
response = client.put("/parts/1", json={"name": "Updated Part"})
assert response.status_code in (403, 405, 404)

View File

@@ -1,6 +1,7 @@
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
class PedimentoConfigAdditionalBase(BaseModel):

View File

@@ -1,6 +1,7 @@
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
class PedimentoConfigCalculationsBase(BaseModel):

View File

@@ -1,7 +1,8 @@
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
from decimal import Decimal
from datetime import datetime
from decimal import Decimal
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
class PedimentoConfigParametersBase(BaseModel):

View File

@@ -1,6 +1,7 @@
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
class PedimentoConfigSurchargesBase(BaseModel):

View File

@@ -1,17 +1,16 @@
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
from api.v1.common.dto_mixins import UpdateFlagsMixin
class PedimentoConfigUpdateRectificationBase(BaseModel):
class PedimentoConfigUpdateRectificationBase(BaseModel, UpdateFlagsMixin):
"""Base schema for Pedimento Config Update Rectification"""
pedimento_id: int = Field(..., description="Pedimento ID")
tenant_id: int = Field(..., description="Tenant ID")
update_vat: Optional[int] = Field(None, description="Update VAT")
update_advalorem: Optional[int] = Field(None, description="Update advalorem")
update_cc: Optional[int] = Field(None, description="Update CC")
update_ieps: Optional[int] = Field(None, description="Update IEPS")
calculate_surcharge: Optional[int] = Field(None, description="Calculate surcharge")
@@ -21,13 +20,9 @@ class PedimentoConfigUpdateRectificationCreate(PedimentoConfigUpdateRectificatio
pass
class PedimentoConfigUpdateRectificationUpdate(BaseModel):
class PedimentoConfigUpdateRectificationUpdate(BaseModel, UpdateFlagsMixin):
"""Schema for updating a Pedimento Config Update Rectification"""
update_vat: Optional[int] = None
update_advalorem: Optional[int] = None
update_cc: Optional[int] = None
update_ieps: Optional[int] = None
calculate_surcharge: Optional[int] = None

View File

@@ -1,18 +1,16 @@
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
from backend.api.v1.common.dto_mixins import UpdateFlagsMixin
class PedimentoConfigUpdatesBase(BaseModel):
class PedimentoConfigUpdatesBase(BaseModel, UpdateFlagsMixin):
"""Base schema for Pedimento Config Updates"""
pedimento_id: int = Field(..., description="Pedimento ID")
tenant_id: int = Field(..., description="Tenant ID")
update_vat: Optional[int] = Field(None, description="Update VAT")
update_advalorem: Optional[int] = Field(None, description="Update advalorem")
update_cc: Optional[int] = Field(None, description="Update CC")
update_ieps: Optional[int] = Field(None, description="Update IEPS")
class PedimentoConfigUpdatesCreate(PedimentoConfigUpdatesBase):
"""Schema for creating a new Pedimento Config Updates"""
@@ -20,13 +18,9 @@ class PedimentoConfigUpdatesCreate(PedimentoConfigUpdatesBase):
pass
class PedimentoConfigUpdatesUpdate(BaseModel):
class PedimentoConfigUpdatesUpdate(BaseModel, UpdateFlagsMixin):
"""Schema for updating a Pedimento Config Updates"""
update_vat: Optional[int] = None
update_advalorem: Optional[int] = None
update_cc: Optional[int] = None
update_ieps: Optional[int] = None
pass
class PedimentoConfigUpdatesResponse(PedimentoConfigUpdatesBase):

View File

@@ -1,6 +1,7 @@
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
class PedimentoCustomsOfficesBase(BaseModel):

View File

@@ -1,6 +1,7 @@
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
from datetime import datetime, time
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
class PedimentoDatesBase(BaseModel):

View File

@@ -1,7 +1,8 @@
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
from decimal import Decimal
from datetime import datetime
from decimal import Decimal
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
class PedimentoDecrementablesBase(BaseModel):

View File

@@ -1,7 +1,8 @@
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
from decimal import Decimal
from datetime import datetime
from decimal import Decimal
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
class PedimentoIncrementablesBase(BaseModel):

View File

@@ -1,7 +1,8 @@
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
from decimal import Decimal
from datetime import datetime
from decimal import Decimal
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
class PedimentoIndexesBase(BaseModel):

View File

@@ -1,6 +1,9 @@
from pydantic import BaseModel, Field, ConfigDict
from datetime import date as Date
from datetime import datetime
from datetime import time as Time
from typing import Optional
from datetime import datetime, date as Date, time as Time
from pydantic import BaseModel, ConfigDict, Field
class PedimentoPaymentsBase(BaseModel):

View File

@@ -1,6 +1,7 @@
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
class PedimentoRectificationDestinationBase(BaseModel):
"""Base schema for Pedimento Rectification Destination"""

View File

@@ -1,6 +1,7 @@
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
class PedimentoRectificationOriginBase(BaseModel):

View File

@@ -1,6 +1,7 @@
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
class PedimentoTransportMeansBase(BaseModel):

View File

@@ -1,6 +1,7 @@
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
class PedimentoValidationBase(BaseModel):

View File

@@ -1,8 +1,9 @@
from enum import IntEnum
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
from decimal import Decimal
from datetime import datetime
from decimal import Decimal
from enum import IntEnum
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
class OperationType(IntEnum):

View File

@@ -1,24 +1,22 @@
from typing import TYPE_CHECKING
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
DateTime,
ForeignKeyConstraint,
Integer,
PrimaryKeyConstraint,
SmallInteger,
String,
UniqueConstraint,
func,
text,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from datetime import datetime
from core.database import Base
if TYPE_CHECKING:
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
class PedimentoConfigAdditional(Base):
class PedimentoConfigAdditional(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "pedimento_config_additional"
__table_args__ = (
PrimaryKeyConstraint("id", name="pedimento_config_additional_pkey"),
@@ -48,8 +46,6 @@ class PedimentoConfigAdditional(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)
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
add_po_identifier: Mapped[int] = mapped_column(SmallInteger)
@@ -59,15 +55,6 @@ class PedimentoConfigAdditional(Base):
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()
)
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_config_additional"
)

View File

@@ -1,24 +1,22 @@
from typing import TYPE_CHECKING
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
DateTime,
ForeignKeyConstraint,
Integer,
PrimaryKeyConstraint,
SmallInteger,
String,
UniqueConstraint,
func,
text,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from datetime import datetime
from core.database import Base
if TYPE_CHECKING:
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
class PedimentoConfigCalculations(Base):
class PedimentoConfigCalculations(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "pedimento_config_calculations"
__table_args__ = (
PrimaryKeyConstraint("id", name="pedimento_config_calculations_pkey"),
@@ -40,8 +38,6 @@ class PedimentoConfigCalculations(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)
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
dta_type: Mapped[str] = mapped_column(String(1))
@@ -55,15 +51,6 @@ class PedimentoConfigCalculations(Base):
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()
)
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_config_calculations"
)

View File

@@ -1,26 +1,23 @@
from decimal import Decimal
from typing import TYPE_CHECKING
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
DateTime,
ForeignKeyConstraint,
Integer,
Numeric,
PrimaryKeyConstraint,
SmallInteger,
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
if TYPE_CHECKING:
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
class PedimentoConfigParameters(Base):
class PedimentoConfigParameters(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "pedimento_config_parameters"
__table_args__ = (
PrimaryKeyConstraint("id", name="pedimento_config_parameters_pkey"),
@@ -50,8 +47,6 @@ class PedimentoConfigParameters(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)
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
is_embassy: Mapped[int] = mapped_column(SmallInteger)
@@ -66,15 +61,6 @@ class PedimentoConfigParameters(Base):
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()
)
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_config_parameters"
)

View File

@@ -1,24 +1,21 @@
from typing import TYPE_CHECKING
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
DateTime,
ForeignKeyConstraint,
Integer,
PrimaryKeyConstraint,
SmallInteger,
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
if TYPE_CHECKING:
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
class PedimentoConfigSurcharges(Base):
class PedimentoConfigSurcharges(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "pedimento_config_surcharges"
__table_args__ = (
PrimaryKeyConstraint("id", name="pedimento_config_surcharges_pkey"),
@@ -48,8 +45,6 @@ class PedimentoConfigSurcharges(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)
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
surcharge_igi: Mapped[int] = mapped_column(SmallInteger)
@@ -59,15 +54,6 @@ class PedimentoConfigSurcharges(Base):
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()
)
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_config_surcharges"
)

View File

@@ -1,24 +1,21 @@
from typing import TYPE_CHECKING
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
DateTime,
ForeignKeyConstraint,
Integer,
PrimaryKeyConstraint,
SmallInteger,
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
if TYPE_CHECKING:
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
class PedimentoConfigUpdateRectification(Base):
class PedimentoConfigUpdateRectification(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "pedimento_config_update_rectification"
__table_args__ = (
PrimaryKeyConstraint("id", name="pedimento_config_update_rectification_pkey"),
@@ -48,8 +45,6 @@ class PedimentoConfigUpdateRectification(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)
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
update_vat: Mapped[int] = mapped_column(SmallInteger)
@@ -58,15 +53,6 @@ class PedimentoConfigUpdateRectification(Base):
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()
)
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_config_update_rectification"
)

View File

@@ -1,24 +1,21 @@
from typing import TYPE_CHECKING
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
DateTime,
ForeignKeyConstraint,
Integer,
PrimaryKeyConstraint,
SmallInteger,
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
if TYPE_CHECKING:
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
class PedimentoConfigUpdates(Base):
class PedimentoConfigUpdates(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "pedimento_config_updates"
__table_args__ = (
PrimaryKeyConstraint("id", name="pedimento_config_updates_pkey"),
@@ -46,8 +43,6 @@ class PedimentoConfigUpdates(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)
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
update_vat: Mapped[int] = mapped_column(SmallInteger)
@@ -55,15 +50,6 @@ class PedimentoConfigUpdates(Base):
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()
)
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_config_updates"
)

View File

@@ -1,24 +1,21 @@
from typing import TYPE_CHECKING
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
DateTime,
ForeignKeyConstraint,
Integer,
PrimaryKeyConstraint,
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
if TYPE_CHECKING:
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
class PedimentoCustomsOffices(Base):
class PedimentoCustomsOffices(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "pedimento_customs_offices"
__table_args__ = (
PrimaryKeyConstraint("id", name="pedimento_customs_offices_pkey"),
@@ -48,22 +45,11 @@ class PedimentoCustomsOffices(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)
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()
)
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_customs_offices"
)

View File

@@ -1,4 +1,9 @@
from datetime import datetime
from datetime import time as datetime_time
from typing import TYPE_CHECKING
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
DateTime,
ForeignKeyConstraint,
@@ -7,19 +12,14 @@ from sqlalchemy import (
PrimaryKeyConstraint,
Time,
UniqueConstraint,
func,
text,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.orm.base import Mapped
from datetime import datetime, time as datetime_time
from core.database import Base
if TYPE_CHECKING:
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
class PedimentoDates(Base):
class PedimentoDates(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "pedimento_dates"
__table_args__ = (
PrimaryKeyConstraint("id", name="pedimento_dates_pkey"),
@@ -46,8 +46,6 @@ class PedimentoDates(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)
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
entry_date: Mapped[datetime] = mapped_column(DateTime)
@@ -63,15 +61,6 @@ class PedimentoDates(Base):
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()
)
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_dates"
)

View File

@@ -1,7 +1,9 @@
from decimal import Decimal
from typing import TYPE_CHECKING
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
DateTime,
ForeignKeyConstraint,
Integer,
Numeric,
@@ -9,19 +11,14 @@ from sqlalchemy import (
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
if TYPE_CHECKING:
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
class PedimentoDecrementables(Base):
class PedimentoDecrementables(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "pedimento_decrementables"
__table_args__ = (
PrimaryKeyConstraint("id", name="pedimento_decrementables_pkey"),
@@ -49,8 +46,6 @@ class PedimentoDecrementables(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)
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
freight: Mapped[Decimal] = mapped_column(Numeric(13, 2))
@@ -63,15 +58,6 @@ class PedimentoDecrementables(Base):
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()
)
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_decrementables"
)

View File

@@ -1,7 +1,9 @@
from decimal import Decimal
from typing import TYPE_CHECKING
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
DateTime,
ForeignKeyConstraint,
Integer,
Numeric,
@@ -9,19 +11,14 @@ from sqlalchemy import (
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
if TYPE_CHECKING:
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
class PedimentoIncrementables(Base):
class PedimentoIncrementables(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "pedimento_incrementables"
__table_args__ = (
PrimaryKeyConstraint("id", name="pedimento_incrementables_pkey"),
@@ -49,8 +46,6 @@ class PedimentoIncrementables(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)
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
insured_value: Mapped[Decimal] = mapped_column(Numeric(13, 2))
@@ -64,15 +59,6 @@ class PedimentoIncrementables(Base):
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()
)
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_incrementables"
)

View File

@@ -1,26 +1,23 @@
from decimal import Decimal
from typing import TYPE_CHECKING
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
DateTime,
ForeignKeyConstraint,
Integer,
Numeric,
PrimaryKeyConstraint,
SmallInteger,
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
if TYPE_CHECKING:
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
class PedimentoIndexes(Base):
class PedimentoIndexes(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "pedimento_indexes"
__table_args__ = (
PrimaryKeyConstraint("id", name="pedimento_indexes_pkey"),
@@ -46,23 +43,12 @@ class PedimentoIndexes(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)
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()
)
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_indexes"
)

View File

@@ -1,7 +1,11 @@
from datetime import date as Date2
from datetime import time as Time2
from typing import TYPE_CHECKING
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
Date,
DateTime,
ForeignKeyConstraint,
Index,
Integer,
@@ -10,19 +14,14 @@ from sqlalchemy import (
String,
Time,
UniqueConstraint,
func,
text,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.orm.base import Mapped
from datetime import datetime, time as Time2, date as Date2
from core.database import Base
if TYPE_CHECKING:
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
class PedimentoPayments(Base):
class PedimentoPayments(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "pedimento_payments"
__table_args__ = (
PrimaryKeyConstraint("id", name="pedimento_payments_pkey"),
@@ -49,8 +48,6 @@ class PedimentoPayments(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)
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
payment_id: Mapped[int] = mapped_column(Integer)
@@ -66,15 +63,6 @@ class PedimentoPayments(Base):
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()
)
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_payments"
)

View File

@@ -1,23 +1,21 @@
from datetime import datetime
from typing import TYPE_CHECKING
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
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
if TYPE_CHECKING:
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
class PedimentoRectificationDestination(Base):
class PedimentoRectificationDestination(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "pedimento_rectification_destination"
__table_args__ = (
PrimaryKeyConstraint("id", name="pedimento_rectification_destination_pkey"),
@@ -47,8 +45,6 @@ class PedimentoRectificationDestination(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)
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
destination_pedimento_year: Mapped[str] = mapped_column(String(2))
@@ -56,15 +52,6 @@ class PedimentoRectificationDestination(Base):
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,8 @@
from datetime import datetime
from typing import TYPE_CHECKING
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
DateTime,
ForeignKeyConstraint,
@@ -8,17 +11,14 @@ from sqlalchemy import (
SmallInteger,
String,
UniqueConstraint,
func,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.orm.base import Mapped
from core.database import Base
if TYPE_CHECKING:
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
class PedimentoRectificationOrigin(Base):
class PedimentoRectificationOrigin(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "pedimento_rectification_origin"
__table_args__ = (
PrimaryKeyConstraint("id", name="pedimento_rectification_origin_pkey"),
@@ -48,8 +48,6 @@ class PedimentoRectificationOrigin(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)
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
original_pedimento_year: Mapped[str] = mapped_column(String(2))
@@ -68,15 +66,6 @@ class PedimentoRectificationOrigin(Base):
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,24 +1,22 @@
from datetime import datetime
from typing import TYPE_CHECKING
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
DateTime,
ForeignKeyConstraint,
Integer,
PrimaryKeyConstraint,
SmallInteger,
String,
UniqueConstraint,
text,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.orm.base import Mapped
from core.database import Base
if TYPE_CHECKING:
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
class PedimentoTransportMeans(Base):
class PedimentoTransportMeans(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "pedimento_transport_means"
__table_args__ = (
PrimaryKeyConstraint("id", name="pedimento_transport_means_pkey"),
@@ -48,17 +46,12 @@ class PedimentoTransportMeans(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)
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

@@ -1,24 +1,21 @@
from typing import TYPE_CHECKING
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
DateTime,
ForeignKeyConstraint,
Integer,
PrimaryKeyConstraint,
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
if TYPE_CHECKING:
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
class PedimentoValidation(Base):
class PedimentoValidation(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "pedimento_validation" # PedimentoValidacion
__table_args__ = (
PrimaryKeyConstraint("id", name="pedimento_validation_pkey"),
@@ -39,8 +36,6 @@ class PedimentoValidation(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)
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
validator: Mapped[str] = mapped_column(String(3)) # validador
@@ -52,15 +47,6 @@ class PedimentoValidation(Base):
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)
pedimento: Mapped["Pedimentos"] = relationship(
"Pedimentos", back_populates="pedimento_validation"
)

View File

@@ -1,7 +1,9 @@
from decimal import Decimal
from typing import TYPE_CHECKING, Optional
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
DateTime,
ForeignKeyConstraint,
Index,
Integer,
@@ -9,14 +11,8 @@ from sqlalchemy import (
PrimaryKeyConstraint,
String,
UniqueConstraint,
func,
text,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.orm.base import Mapped
from datetime import datetime
from enum import IntEnum
from core.database import Base
if TYPE_CHECKING:
from api.v1.modules.a76.pedmientos.models.pedimento_config_additional import (
@@ -65,7 +61,7 @@ if TYPE_CHECKING:
)
class Pedimentos(Base):
class Pedimentos(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "pedimentos"
__table_args__ = (
PrimaryKeyConstraint("id", name="pedimentos_pkey"),
@@ -102,8 +98,6 @@ 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))
@@ -120,15 +114,6 @@ class Pedimentos(Base):
gross_weight: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 3))
exchange_rate: Mapped[Optional[Decimal]] = mapped_column(Numeric(9, 5))
# 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_config_additional: Mapped["PedimentoConfigAdditional"] = relationship(
"PedimentoConfigAdditional", uselist=False, back_populates="pedimento"
)

View File

@@ -2,95 +2,25 @@
Routes for PedimentoConfigAdditional CRUD operations
"""
from typing import Dict, Any
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from core.database import get_core_db
from core.security import validate_access_to_resource, get_current_user
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from ..services.pedimento_config_additional import PedimentoConfigAdditionalService
from ..dtos.pedimento_config_additional import (
PedimentoConfigAdditionalCreate,
PedimentoConfigAdditionalUpdate,
PedimentoConfigAdditionalResponse,
PedimentoConfigAdditionalUpdate,
)
from ..services.pedimento_config_additional import PedimentoConfigAdditionalService
router = APIRouter(prefix="/{pedimento_id}/config-additional")
@router.get("/", response_model=PedimentoConfigAdditionalResponse)
async def get_config_additional(
pedimento_id: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Get config additional by pedimento ID"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
config = PedimentoConfigAdditionalService.get_by_pedimento_id(
db, pedimento_id, tenant_id, company_id
)
if not config:
raise HTTPException(status_code=404, detail="Config additional not found")
return config
@router.post("/", response_model=PedimentoConfigAdditionalResponse, status_code=201)
async def create_config_additional(
pedimento_id: int,
data: PedimentoConfigAdditionalCreate,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Create config additional"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
# Ensure pedimento_id and company_id match
if data.pedimento_id != pedimento_id:
raise HTTPException(status_code=400, detail="Pedimento ID mismatch")
config = PedimentoConfigAdditionalService.create(db, data, tenant_id, company_id)
return config
@router.put("/", response_model=PedimentoConfigAdditionalResponse)
async def update_config_additional(
pedimento_id: int,
data: PedimentoConfigAdditionalUpdate,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Update config additional"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
config = PedimentoConfigAdditionalService.update(
db, pedimento_id, tenant_id, company_id, data
)
if not config:
raise HTTPException(status_code=404, detail="Config additional not found")
return config
@router.delete("/", status_code=204)
async def delete_config_additional(
pedimento_id: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Delete config additional"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
success = PedimentoConfigAdditionalService.delete(
db, pedimento_id, tenant_id, company_id
)
if not success:
raise HTTPException(status_code=404, detail="Config additional not found")
return None
# Create router with generic CRUD routes for child resource
router = TenantCRUDRoutes(
service=PedimentoConfigAdditionalService,
create_schema=PedimentoConfigAdditionalCreate,
update_schema=PedimentoConfigAdditionalUpdate,
response_schema=PedimentoConfigAdditionalResponse,
prefix="/{pedimento_id}/config-additional",
tags=[],
resource_name="Config additional",
parent_id_name="pedimento_id",
enable_list=False,
validate_parent_match=True,
).router

View File

@@ -2,93 +2,25 @@
Routes for PedimentoConfigCalculations CRUD operations
"""
from typing import Dict, Any
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from core.database import get_core_db
from core.security import validate_access_to_resource, get_current_user
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from ..services.pedimento_config_calculations import PedimentoConfigCalculationsService
from ..dtos.pedimento_config_calculations import (
PedimentoConfigCalculationsCreate,
PedimentoConfigCalculationsUpdate,
PedimentoConfigCalculationsResponse,
PedimentoConfigCalculationsUpdate,
)
from ..services.pedimento_config_calculations import PedimentoConfigCalculationsService
router = APIRouter(prefix="/{pedimento_id}/config-calculations")
@router.get("/", response_model=PedimentoConfigCalculationsResponse)
async def get_config_calculations(
pedimento_id: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Get config calculations by pedimento ID"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
config = PedimentoConfigCalculationsService.get_by_pedimento_id(
db, pedimento_id, tenant_id, company_id
)
if not config:
raise HTTPException(status_code=404, detail="Config calculations not found")
return config
@router.post("/", response_model=PedimentoConfigCalculationsResponse, status_code=201)
async def create_config_calculations(
pedimento_id: int,
data: PedimentoConfigCalculationsCreate,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Create config calculations"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
# Ensure pedimento_id matches
if data.pedimento_id != pedimento_id:
raise HTTPException(status_code=400, detail="Pedimento ID mismatch")
config = PedimentoConfigCalculationsService.create(db, data, tenant_id, company_id)
return config
@router.put("/", response_model=PedimentoConfigCalculationsResponse)
async def update_config_calculations(
pedimento_id: int,
data: PedimentoConfigCalculationsUpdate,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Update config calculations"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
config = PedimentoConfigCalculationsService.update(
db, pedimento_id, tenant_id, company_id, data
)
if not config:
raise HTTPException(status_code=404, detail="Config calculations not found")
return config
@router.delete("/", status_code=204)
async def delete_config_calculations(
pedimento_id: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Delete config calculations"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
success = PedimentoConfigCalculationsService.delete(db, pedimento_id, company_id)
if not success:
raise HTTPException(status_code=404, detail="Config calculations not found")
return None
# Create router with generic CRUD routes for child resource
router = TenantCRUDRoutes(
service=PedimentoConfigCalculationsService,
create_schema=PedimentoConfigCalculationsCreate,
update_schema=PedimentoConfigCalculationsUpdate,
response_schema=PedimentoConfigCalculationsResponse,
prefix="/{pedimento_id}/config-calculations",
tags=[],
resource_name="Config calculations",
parent_id_name="pedimento_id",
enable_list=False,
validate_parent_match=True,
).router

View File

@@ -2,95 +2,25 @@
Routes for PedimentoConfigParameters CRUD operations
"""
from typing import Dict, Any
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from core.database import get_core_db
from core.security import validate_access_to_resource, get_current_user
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from ..services.pedimento_config_parameters import PedimentoConfigParametersService
from ..dtos.pedimento_config_parameters import (
PedimentoConfigParametersCreate,
PedimentoConfigParametersUpdate,
PedimentoConfigParametersResponse,
PedimentoConfigParametersUpdate,
)
from ..services.pedimento_config_parameters import PedimentoConfigParametersService
router = APIRouter(prefix="/{pedimento_id}/config-parameters")
@router.get("/", response_model=PedimentoConfigParametersResponse)
async def get_config_parameters(
pedimento_id: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Get config parameters by pedimento ID"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
config = PedimentoConfigParametersService.get_by_pedimento_id(
db, pedimento_id, tenant_id, company_id
)
if not config:
raise HTTPException(status_code=404, detail="Config parameters not found")
return config
@router.post("/", response_model=PedimentoConfigParametersResponse, status_code=201)
async def create_config_parameters(
pedimento_id: int,
data: PedimentoConfigParametersCreate,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Create config parameters"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
# Ensure pedimento_id matches
if data.pedimento_id != pedimento_id:
raise HTTPException(status_code=400, detail="Pedimento ID mismatch")
config = PedimentoConfigParametersService.create(db, data, tenant_id, company_id)
return config
@router.put("/", response_model=PedimentoConfigParametersResponse)
async def update_config_parameters(
pedimento_id: int,
data: PedimentoConfigParametersUpdate,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Update config parameters"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
config = PedimentoConfigParametersService.update(
db, pedimento_id, tenant_id, company_id, data
)
if not config:
raise HTTPException(status_code=404, detail="Config parameters not found")
return config
@router.delete("/", status_code=204)
async def delete_config_parameters(
pedimento_id: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Delete config parameters"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
success = PedimentoConfigParametersService.delete(
db, pedimento_id, tenant_id, company_id
)
if not success:
raise HTTPException(status_code=404, detail="Config parameters not found")
return None
# Create router with generic CRUD routes for child resource
router = TenantCRUDRoutes(
service=PedimentoConfigParametersService,
create_schema=PedimentoConfigParametersCreate,
update_schema=PedimentoConfigParametersUpdate,
response_schema=PedimentoConfigParametersResponse,
prefix="/{pedimento_id}/config-parameters",
tags=[],
resource_name="Config parameters",
parent_id_name="pedimento_id",
enable_list=False,
validate_parent_match=True,
).router

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