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:
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
30
backend/api/v1/common/base_models.py
Normal file
30
backend/api/v1/common/base_models.py
Normal 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)
|
||||
121
backend/api/v1/common/crud_routes.py
Normal file
121
backend/api/v1/common/crud_routes.py
Normal 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
|
||||
31
backend/api/v1/common/dto_mixins.py
Normal file
31
backend/api/v1/common/dto_mixins.py
Normal 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")
|
||||
350
backend/api/v1/common/tenant_crud_routes.py
Normal file
350
backend/api/v1/common/tenant_crud_routes.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
class CustomsBrokerDTO(BaseModel):
|
||||
type: Optional[str]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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]:
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,7 +3,8 @@ Service layer for CountryRuleOct.
|
||||
"""
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from . import models, dto
|
||||
|
||||
from . import dto, models
|
||||
|
||||
|
||||
class CountryRuleOctService:
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ExchangeRateBaseDTO(BaseModel):
|
||||
date: int
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from . import models, dto
|
||||
|
||||
from . import dto, models
|
||||
|
||||
|
||||
class ExchangeRateService:
|
||||
|
||||
@@ -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)
|
||||
@@ -2,9 +2,10 @@
|
||||
DTOs for FractionRuleOctave.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class FractionRuleOctaveBaseDTO(BaseModel):
|
||||
PERMISSION: str
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from . import models, dto
|
||||
|
||||
from . import dto, models
|
||||
|
||||
"""
|
||||
Service layer for FractionRuleOctave.
|
||||
|
||||
@@ -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)
|
||||
@@ -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):
|
||||
|
||||
@@ -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})>"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
DTOs for GBultos.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class GBultoBaseDTO(BaseModel):
|
||||
CODE: str
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from . import models, dto
|
||||
|
||||
from . import dto, models
|
||||
|
||||
|
||||
class GBultoService:
|
||||
|
||||
@@ -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)
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user