Merge pull request 'fix/permisos' (#338) from fix/permisos into development

Reviewed-on: ADUANASOFT/anexo76#338
This commit is contained in:
2026-04-27 21:55:11 +00:00
342 changed files with 18899 additions and 13086 deletions

View File

@@ -78,12 +78,7 @@ from api.v1.modules.a76.general_catalogs.fractions.tariff_fractions.seed import
from api.v1.modules.public.reference_data.trailer_types.seed import (
seed as trailer_types_seed,
)
from api.v1.modules.core.permissions.seed import (
seed_invoices,
seed_user,
seed_report,
seed_roles,
)
from api.v1.modules.core.permissions.seed_v2 import registry
from api.v1.modules.public.reference_data.license_exceptions.seed import seed_license_exceptions
from api.v1.modules.public.reference_data.agency_tariff_codes.seed import seed_agency_tariff_codes
@@ -460,13 +455,13 @@ def upgrade() -> None:
# --- SEEDS CORE (Permissions) ---
# Combinar todas las seeds de permisos
all_permissions = seed_invoices + seed_user + seed_report + seed_roles
# Combinar todas las seeds de permisos desde el Registro V2
all_permissions = registry.get_all()
values_permissions = ", ".join(
[
f"({format_value(code)}, {format_value(desc)}, {format_value(module)}, {format_value(action)})"
for code, desc, module, action in all_permissions
f"({format_value(p.code)}, {format_value(p.description)}, {format_value(p.module)}, {format_value(p.action)})"
for p in all_permissions
]
)

View File

@@ -195,19 +195,32 @@ class TenantCRUDRoutes(
if "sort_order" in sig.parameters:
kwargs["sort_order"] = sort_order
items, total = self.service.get_all(
db, tenant_id, target_company_id, skip, page_size, filters, **kwargs
)
try:
items, total = self.service.get_all(
db, tenant_id, target_company_id, skip, page_size, filters, **kwargs
)
except Exception as e:
logger.error(f"Error in {self.resource_name} list service: {str(e)}", exc_info=True)
raise HTTPException(
status_code=500,
detail=f"Error listing {self.resource_name}s: {str(e)}"
)
return {
"items": [
self.response_schema.model_validate(item) for item in items
],
"total": total,
"page": page,
"page_size": page_size,
}
try:
return {
"items": [
self.response_schema.model_validate(item) for item in items
],
"total": total,
"page": page,
"page_size": page_size,
}
except Exception as e:
logger.error(f"Error validating {self.resource_name} response schema: {str(e)}", exc_info=True)
raise HTTPException(
status_code=500,
detail=f"Data validation error in {self.resource_name}"
)
else:
@@ -261,18 +274,32 @@ class TenantCRUDRoutes(
if "sort_order" in sig.parameters:
kwargs["sort_order"] = sort_order
items, total = self.service.get_all(
db, tenant_id, target_company_id, skip, page_size, None, **kwargs
)
try:
items, total = self.service.get_all(
db, tenant_id, target_company_id, skip, page_size, None, **kwargs
)
except Exception as e:
logger.error(f"Error in {self.resource_name} list service: {str(e)}", exc_info=True)
raise HTTPException(
status_code=500,
detail=f"Error listing {self.resource_name}s: {str(e)}"
)
return {
"items": [
self.response_schema.model_validate(item) for item in items
],
"total": total,
"page": page,
"page_size": page_size,
}
try:
return {
"items": [
self.response_schema.model_validate(item) for item in items
],
"total": total,
"page": page,
"page_size": page_size,
}
except Exception as e:
logger.error(f"Error validating {self.resource_name} response schema: {str(e)}", exc_info=True)
raise HTTPException(
status_code=500,
detail=f"Data validation error in {self.resource_name}"
)
# GET single resource route
# For parent resources: GET /{id}
@@ -336,9 +363,16 @@ class TenantCRUDRoutes(
db, company_id, current_user, self.get_permissions, self.require_all
)
resource = self.service.get_by_id(
db, resource_id, tenant_id, company_id
)
try:
resource = self.service.get_by_id(
db, resource_id, tenant_id, company_id
)
except Exception as e:
logger.error(f"Error in {self.resource_name} get service: {str(e)}", exc_info=True)
raise HTTPException(
status_code=500,
detail=f"Error retrieving {self.resource_name}: {str(e)}"
)
if not resource:
raise HTTPException(

View File

@@ -4,6 +4,7 @@ from typing import Optional, Dict, Any
from core.database import get_core_db
from .service import AppSettingsService
from .schemas import AppSettingRequest, AppSettingResponse
from core.security import get_current_user, validate_access_to_resource
router = APIRouter(prefix="/a76/app-settings", tags=["a76 / app_settings"])
@@ -16,14 +17,20 @@ logger = logging.getLogger(__name__)
def get_resolved_settings(
tenant_id: int = Query(...),
company_id: int = Query(...),
db: Session = Depends(get_core_db)
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user)
):
"""
Returns the final merged configuration for a company.
Merges Global -> Tenant -> Company levels.
"""
try:
# Validar permisos
validate_access_to_resource(db, company_id, current_user, ["settings_general.view"])
return AppSettingsService.get_resolved_settings(db, tenant_id, company_id)
except HTTPException:
raise
except Exception as e:
logger.error(f"RESOLVE ERROR: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@@ -31,12 +38,16 @@ def get_resolved_settings(
@router.post("/upsert")
def upsert_settings(
payload: AppSettingRequest,
db: Session = Depends(get_core_db)
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user)
):
"""
Creates or updates an override for a specific level (Global, Tenant, or Company).
"""
try:
# Validar permisos
validate_access_to_resource(db, payload.company_id, current_user, ["settings_general.edit"])
data = payload.settings.model_dump(exclude_unset=True)
return AppSettingsService.upsert_settings(
db,
@@ -44,6 +55,8 @@ def upsert_settings(
payload.company_id,
data
)
except HTTPException:
raise
except Exception as e:
logger.error(f"UPSERT ERROR: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@@ -51,9 +64,10 @@ def upsert_settings(
@router.put("/upsert")
def update_settings(
payload: AppSettingRequest,
db: Session = Depends(get_core_db)
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user)
):
"""
Alias for upsert_settings.
"""
return upsert_settings(payload, db)
return upsert_settings(payload, db, current_user)

View File

@@ -42,7 +42,7 @@ async def get_classes_with_fa_data(
Get all classes with their FA data using a single LEFT JOIN query.
This endpoint is optimized for the fixed-asset-classes view.
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["goods_classes.view"])
skip = (page - 1) * page_size
@@ -74,7 +74,7 @@ async def create_fa_class(
):
"""Create a fixed asset class (both base class and FA extension)"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["goods_classes.create"])
result = ClassService.create_fa_class(db, class_data, tenant_id, company_id)
@@ -95,6 +95,10 @@ crud_router = TenantCRUDRoutes(
enable_filters=True,
default_page_size=50,
max_page_size=1000,
list_permissions=["goods_classes.view"],
create_permissions=["goods_classes.create"],
update_permissions=["goods_classes.edit"],
delete_permissions=["goods_classes.delete"],
).router
# Include the CRUD routes into our main router

View File

@@ -49,7 +49,7 @@ async def get_clients_and_providers(
current_user: dict = Depends(get_current_user),
):
"""Get clients and providers"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["partners_mgmt.view"])
resolved_limit = page_size if page_size is not None else limit
resolved_skip = ((page - 1) * resolved_limit) if page is not None else skip
@@ -98,7 +98,7 @@ async def get_clients_and_providers_basic_info(
current_user: dict = Depends(get_current_user),
):
"""Get basic information for a client/provider (without address and programs)"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["partners_mgmt.view"])
client = ClientProviderService.get_by_id(db, client_id, tenant_id, company_id)
if not client:
@@ -115,7 +115,7 @@ async def create_client_provider(
current_user: dict = Depends(get_current_user),
):
"""Create a new client/provider"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["partners_mgmt.create"])
return ClientProviderService.create(db, client_data, tenant_id, company_id)
@@ -129,7 +129,7 @@ async def update_client_provider(
current_user: dict = Depends(get_current_user),
):
"""Update a client/provider"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["partners_mgmt.edit"])
client = ClientProviderService.update(
db, client_id, tenant_id, company_id, client_data
@@ -150,7 +150,7 @@ async def get_client_provider_detail(
Obtener un cliente/proveedor completo por ID.
Esta es la ruta que tu formulario necesita para cargar los datos.
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["partners_mgmt.view"])
# Usamos el servicio para buscar por ID
client = ClientProviderService.get_by_id(db, client_id, tenant_id, company_id)
@@ -168,7 +168,7 @@ async def delete_client_provider(
current_user: dict = Depends(get_current_user),
):
"""Delete a client/provider"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["partners_mgmt.delete"])
success = ClientProviderService.delete(db, client_id, tenant_id, company_id)
if not success:

View File

@@ -78,6 +78,11 @@ customs_broker_crud = TenantCRUDRoutes(
id_name="broker_key",
id_type=str,
enable_list=True,
list_permissions=["customs_brokers.view"],
get_permissions=["customs_brokers.view"],
create_permissions=["customs_brokers.create"],
update_permissions=["customs_brokers.edit"],
delete_permissions=["customs_brokers.delete"],
)
router.include_router(customs_broker_crud.router)
@@ -97,7 +102,7 @@ def update_customs_broker(
"""
Actualización parcial (PATCH).
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["customs_brokers.edit"])
broker = services.CustomsBrokerService.get_by_id(db, broker_key, tenant_id, company_id)
if not broker:
@@ -128,7 +133,7 @@ def update_customs_broker_vu(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["customs_brokers.edit"])
broker = services.CustomsBrokerService.get_by_id(db, broker_key, tenant_id, company_id)
if not broker:
@@ -152,7 +157,7 @@ def update_customs_broker_personnel(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["customs_brokers.edit"])
broker = services.CustomsBrokerService.get_by_id(db, broker_key, tenant_id, company_id)
if not broker:
@@ -192,7 +197,7 @@ async def upload_customs_broker_vu_file(
- VU: certificate_path, key_path, xml_files_path
- DODA: doda_certificate_path, doda_key_path, doda_xml_files_path
"""
validate_access_to_resource(db, company_id, current_user)
validate_access_to_resource(db, company_id, current_user, ["customs_brokers.create"])
tenant_id = _resolve_tenant_id_int(current_user)
broker = services.CustomsBrokerService.get_by_id(db, broker_key, tenant_id, company_id)

View File

@@ -11,5 +11,10 @@ router = TenantCRUDRoutes(
tags=["a76.general_catalogs.classification_concepts"],
resource_name="Classification Concept",
enable_list=True,
list_permissions=["cat_classification.view"],
get_permissions=["cat_classification.view"],
create_permissions=["cat_classification.create"],
update_permissions=["cat_classification.edit"],
delete_permissions=["cat_classification.delete"],
enable_filters=True,
).router

View File

@@ -11,4 +11,9 @@ router = TenantCRUDRoutes(
tags=["a76.general_catalogs.concepts"],
resource_name="Concept",
enable_list=True,
list_permissions=["cat_concepts.view"],
get_permissions=["cat_concepts.view"],
create_permissions=["cat_concepts.create"],
update_permissions=["cat_concepts.edit"],
delete_permissions=["cat_concepts.delete"],
).router

View File

@@ -21,6 +21,11 @@ router = TenantCRUDRoutes(
enable_filters=True, # Enable filtering
default_page_size=50,
max_page_size=100,
list_permissions=["cat_broker_concepts.view"],
get_permissions=["cat_broker_concepts.view"],
create_permissions=["cat_broker_concepts.create"],
update_permissions=["cat_broker_concepts.edit"],
delete_permissions=["cat_broker_concepts.delete"],
).router

View File

@@ -7,9 +7,8 @@ from .dto import (
from .service import EquivalencyService, EquivalencyItemService
router = APIRouter(prefix="/equivalencies",
tags=["a76.general_catalogs.equivalencies"])
tags=["a76.general_catalogs.equivalencies"])
# Pool global de EquivalencyItems
item_crud = TenantCRUDRoutes(
service=EquivalencyItemService,
create_schema=EquivalencyItemCreate,
@@ -20,9 +19,13 @@ item_crud = TenantCRUDRoutes(
resource_name="EquivalencyItem",
enable_list=True,
enable_filters=True,
list_permissions=["cat_equivalencies.view"],
get_permissions=["cat_equivalencies.view"],
create_permissions=["cat_equivalencies.create"],
update_permissions=["cat_equivalencies.edit"],
delete_permissions=["cat_equivalencies.delete"],
)
# Catálogo de equivalencias (referencia item_id)
equivalency_crud = TenantCRUDRoutes(
service=EquivalencyService,
create_schema=EquivalencyCreate,
@@ -33,7 +36,12 @@ equivalency_crud = TenantCRUDRoutes(
resource_name="Equivalency",
enable_list=True,
enable_filters=True,
list_permissions=["cat_equivalencies.view"],
get_permissions=["cat_equivalencies.view"],
create_permissions=["cat_equivalencies.create"],
update_permissions=["cat_equivalencies.edit"],
delete_permissions=["cat_equivalencies.delete"],
)
router.include_router(item_crud.router)
router.include_router(equivalency_crud.router)
router.include_router(equivalency_crud.router)

View File

@@ -25,7 +25,6 @@ from .service import ErrorClassificationService, ErrorCatalogService
router = APIRouter(prefix="/error-catalogs", tags=["error-catalogs"])
# ============ ERROR CLASSIFICATIONS ENDPOINTS ============
classification_crud = TenantCRUDRoutes(
service=ErrorClassificationService,
@@ -95,7 +94,6 @@ async def get_classification(
router.include_router(classification_crud.router)
# ============ ERROR CATALOG ENDPOINTS ============
catalog_crud = TenantCRUDRoutes(
service=ErrorCatalogService,

View File

@@ -25,6 +25,12 @@ route_handler = TenantCRUDRoutes(
enable_filters=False,
default_page_size=50,
max_page_size=100,
# Permisos
list_permissions=["cat_exchange_rates.view"],
get_permissions=["cat_exchange_rates.view"],
create_permissions=["cat_exchange_rates.create"],
update_permissions=["cat_exchange_rates.edit"],
delete_permissions=["cat_exchange_rates.delete"],
)
crud_router = route_handler.router
@@ -68,7 +74,7 @@ async def list_exchange_rates(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["cat_exchange_rates.view"])
skip = (page - 1) * page_size
filters = {}

View File

@@ -27,7 +27,8 @@ async def list_fda_catalog(
current_user: Dict[str, Any] = Depends(get_current_user)
):
"""Listar entradas del catálogo FDA con búsqueda y paginación"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
# 👇 PERMISO DE LECTURA 👇
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["goods_fda.view"])
filters = {"search": search} if search else {}
result = FDACatalogService.get_all(db, tenant_id, company_id, page, page_size, filters)
@@ -49,7 +50,8 @@ async def get_fda_catalog(
current_user: Dict[str, Any] = Depends(get_current_user)
):
"""Obtener una entrada del catálogo FDA por ID"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
# 👇 PERMISO DE LECTURA 👇
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["goods_fda.view"])
entry = FDACatalogService.get_by_id(db, tenant_id, company_id, id)
if not entry:
@@ -68,6 +70,7 @@ async def get_fda_catalog(
"call_atl": entry.call_atl
}
from api.v1.modules.a76.general_catalogs.fda_catalog.dto import FDACatalogCreate, FDACatalogUpdate
from sqlalchemy.exc import IntegrityError
@@ -79,7 +82,8 @@ async def create_fda_catalog(
current_user: Dict[str, Any] = Depends(get_current_user)
):
"""Crear nueva entrada en el catálogo FDA"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
# 👇 PERMISO DE CREACIÓN 👇
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["goods_fda.create"])
try:
return FDACatalogService.create(db, tenant_id, company_id, data)
except IntegrityError:
@@ -96,7 +100,8 @@ async def update_fda_catalog(
current_user: Dict[str, Any] = Depends(get_current_user)
):
"""Actualizar entrada del catálogo FDA"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
# 👇 PERMISO DE EDICIÓN 👇
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["goods_fda.edit"])
try:
entry = FDACatalogService.update(db, tenant_id, company_id, id, data)
@@ -116,12 +121,14 @@ async def delete_fda_catalog(
current_user: Dict[str, Any] = Depends(get_current_user)
):
"""Eliminar entrada del catálogo FDA"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
# 👇 PERMISO DE ELIMINACIÓN 👇
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["goods_fda.delete"])
success = FDACatalogService.delete(db, tenant_id, company_id, id)
if not success:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Entrada no encontrada")
return {"message": "Eliminado correctamente"}
from typing import List
from api.v1.modules.a76.general_catalogs.fda_catalog.service import FDADetailsService
from api.v1.modules.a76.general_catalogs.fda_catalog.dto import (
@@ -139,7 +146,7 @@ async def get_fda_specifications(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user)
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["goods_fda.view"])
catalog_entry = FDACatalogService.get_by_id(db, tenant_id, company_id, id)
if not catalog_entry:
raise HTTPException(status_code=404, detail="Catálogo FDA no encontrado")
@@ -154,7 +161,8 @@ async def save_fda_specifications(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user)
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
# Aquí usamos .edit porque estamos modificando un catálogo existente
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["goods_fda.edit"])
catalog_entry = FDACatalogService.get_by_id(db, tenant_id, company_id, id)
if not catalog_entry:
raise HTTPException(status_code=404, detail="Catálogo FDA no encontrado")
@@ -169,7 +177,7 @@ async def list_fda_constituent_elements(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user)
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["goods_fda.view"])
return FDADetailsService.get_constituent_elements(db, tenant_id, company_id, id)
@router.post("/{id}/constituent-elements", response_model=FDAConstituentElementsResponse)
@@ -180,7 +188,7 @@ async def create_fda_constituent_element(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user)
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["goods_fda.create"])
return FDADetailsService.create_constituent_element(db, tenant_id, company_id, id, data.model_dump())
@router.delete("/{id}/constituent-elements/{element_id}")
@@ -191,7 +199,7 @@ async def delete_fda_constituent_element(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user)
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["goods_fda.delete"])
success = FDADetailsService.delete_constituent_element(db, tenant_id, company_id, element_id)
if not success:
raise HTTPException(status_code=404, detail="Elemento no encontrado")
@@ -205,7 +213,7 @@ async def list_fda_affirmation_codes(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user)
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["goods_fda.view"])
return FDADetailsService.get_affirmation_codes(db, tenant_id, company_id, id)
@router.post("/{id}/affirmation-codes", response_model=FDAAffirmationCodesResponse)
@@ -216,7 +224,7 @@ async def create_fda_affirmation_code(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user)
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["goods_fda.create"])
return FDADetailsService.create_affirmation_code(db, tenant_id, company_id, id, data.model_dump())
@router.delete("/{id}/affirmation-codes/{code_id}")
@@ -227,7 +235,7 @@ async def delete_fda_affirmation_code(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user)
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["goods_fda.delete"])
success = FDADetailsService.delete_affirmation_code(db, tenant_id, company_id, code_id)
if not success:
raise HTTPException(status_code=404, detail="Código no encontrado")
@@ -241,7 +249,7 @@ async def list_fda_lot_productions(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user)
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["goods_fda.view"])
return FDADetailsService.get_lot_productions(db, tenant_id, company_id, id)
@router.post("/{id}/lot-productions", response_model=FDALotProductionResponse)
@@ -252,7 +260,7 @@ async def create_fda_lot_production(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user)
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["goods_fda.create"])
return FDADetailsService.create_lot_production(db, tenant_id, company_id, id, data.model_dump())
@router.delete("/{id}/lot-productions/{lot_id}")
@@ -263,8 +271,8 @@ async def delete_fda_lot_production(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user)
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["goods_fda.delete"])
success = FDADetailsService.delete_lot_production(db, tenant_id, company_id, lot_id)
if not success:
raise HTTPException(status_code=404, detail="Lote no encontrado")
return {"message": "Eliminado correctamente"}
return {"message": "Eliminado correctamente"}

View File

@@ -25,6 +25,8 @@ def list_canadian_fractions(
db: Session = Depends(get_core_db),
current_user = Depends(get_current_user)
):
from core.security import validate_access_to_resource as validate_perm
validate_perm(db, company_id, current_user, ["frac_canadian.view"])
tenant_id = validate_access_to_resource(db, company_id, current_user)
skip = (page - 1) * page_size
service = CanadianTariffFractionService(db)
@@ -50,6 +52,8 @@ def get_canadian_fraction(
db: Session = Depends(get_core_db),
current_user = Depends(get_current_user)
):
from core.security import validate_access_to_resource as validate_perm
validate_perm(db, company_id, current_user, ["frac_canadian.view"])
tenant_id = validate_access_to_resource(db, company_id, current_user)
service = CanadianTariffFractionService(db)
item = service.get(id, tenant_id, company_id)
@@ -64,6 +68,8 @@ def create_canadian_fraction(
db: Session = Depends(get_core_db),
current_user = Depends(get_current_user)
):
from core.security import validate_access_to_resource as validate_perm
validate_perm(db, company_id, current_user, ["frac_canadian.create"])
tenant_id = validate_access_to_resource(db, company_id, current_user)
service = CanadianTariffFractionService(db)
return service.create(item_in, tenant_id, company_id)
@@ -76,6 +82,8 @@ def update_canadian_fraction(
db: Session = Depends(get_core_db),
current_user = Depends(get_current_user)
):
from core.security import validate_access_to_resource as validate_perm
validate_perm(db, company_id, current_user, ["frac_canadian.edit"])
tenant_id = validate_access_to_resource(db, company_id, current_user)
service = CanadianTariffFractionService(db)
item = service.get(id, tenant_id, company_id)
@@ -90,6 +98,8 @@ def delete_canadian_fraction(
db: Session = Depends(get_core_db),
current_user = Depends(get_current_user)
):
from core.security import validate_access_to_resource as validate_perm
validate_perm(db, company_id, current_user, ["frac_canadian.delete"])
tenant_id = validate_access_to_resource(db, company_id, current_user)
service = CanadianTariffFractionService(db)
item = service.get(id, tenant_id, company_id)

View File

@@ -20,9 +20,8 @@ def get_historical_fractions(
db: Session = Depends(get_core_db),
current_user = Depends(get_current_user)
):
"""
Get all historical tariff fractions (paginated).
"""
from core.security import validate_access_to_resource as validate_perm
validate_perm(db, company_id, current_user, ["frac_historical.view"])
tenant_id = validate_access_to_resource(db, company_id, current_user)
skip = (page - 1) * page_size
service = HistoricalTariffFractionService(db)
@@ -47,9 +46,8 @@ async def get_rate(
db: Session = Depends(get_core_db),
current_user = Depends(get_current_user)
):
"""
Get the historical tariff rate for a specific fraction, nico, and date.
"""
from core.security import validate_access_to_resource as validate_perm
validate_perm(db, company_id, current_user, ["frac_historical.view"])
tenant_id = validate_access_to_resource(db, company_id, current_user)
# Parse date
@@ -82,9 +80,8 @@ def get_historical_fraction(
db: Session = Depends(get_core_db),
current_user = Depends(get_current_user)
):
"""
Get a historical tariff fraction by ID.
"""
from core.security import validate_access_to_resource as validate_perm
validate_perm(db, company_id, current_user, ["frac_historical.view"])
tenant_id = validate_access_to_resource(db, company_id, current_user)
service = HistoricalTariffFractionService(db)
fraction = service.get(id, tenant_id, company_id)
@@ -99,9 +96,8 @@ def create_historical_fraction(
db: Session = Depends(get_core_db),
current_user = Depends(get_current_user)
):
"""
Create a new historical tariff fraction.
"""
from core.security import validate_access_to_resource as validate_perm
validate_perm(db, company_id, current_user, ["frac_historical.create"])
tenant_id = validate_access_to_resource(db, company_id, current_user)
service = HistoricalTariffFractionService(db)
return service.create(fraction_in, tenant_id, company_id)
@@ -114,9 +110,8 @@ def update_historical_fraction(
db: Session = Depends(get_core_db),
current_user = Depends(get_current_user)
):
"""
Update a historical tariff fraction.
"""
from core.security import validate_access_to_resource as validate_perm
validate_perm(db, company_id, current_user, ["frac_historical.edit"])
tenant_id = validate_access_to_resource(db, company_id, current_user)
service = HistoricalTariffFractionService(db)
fraction = service.get(id, tenant_id, company_id)
@@ -131,9 +126,8 @@ def delete_historical_fraction(
db: Session = Depends(get_core_db),
current_user = Depends(get_current_user)
):
"""
Delete a historical tariff fraction.
"""
from core.security import validate_access_to_resource as validate_perm
validate_perm(db, company_id, current_user, ["frac_historical.delete"])
tenant_id = validate_access_to_resource(db, company_id, current_user)
service = HistoricalTariffFractionService(db)
fraction = service.get(id, tenant_id, company_id)

View File

@@ -8,7 +8,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from core.database import get_core_db
from core.security import get_current_user, get_tenant_from_token
from core.security import get_current_user, get_tenant_from_token, validate_access_to_resource
from .dto import (
TariffFractionCreateDTO,
@@ -39,6 +39,12 @@ async def list_tariff_fractions(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
# Validar permisos según el catálogo solicitado
if catalog in ["mex", "usa"]:
validate_access_to_resource(db, company_id, current_user, ["frac_sitar.view"])
elif catalog == "american":
validate_access_to_resource(db, company_id, current_user, ["frac_american.view"])
skip = (page - 1) * page_size
filters = {}
if search:
@@ -77,9 +83,12 @@ async def list_tariff_fractions(
)
async def get_tariff_fraction(
tariff_fraction_id: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
# Por defecto asumimos vista de SITAR para este endpoint de consulta por ID general
validate_access_to_resource(db, company_id, current_user, ["frac_sitar.view"])
item = TariffFractionService.get_by_id(db, tariff_fraction_id)
if not item:
from fastapi import HTTPException
@@ -100,6 +109,48 @@ async def create_tariff_fraction(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""
Crea una nueva fracción.
- MEX/USA: No permitido (Read-Only)
- AMERICAN: Permitido (Local DB)
"""
from fastapi import HTTPException
tenant_id = current_user.get("tenant_id")
if catalog == "american":
validate_access_to_resource(db, company_id, current_user, ["frac_american.create"])
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.service import USTariffFractionService
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.dto import USTariffFractionCreateDTO
import re
# Map generic DTO to US DTO
ad_valorem = None
if fraction_data.adv_impo:
try:
# remove non-numeric chars except dot
clean = re.sub(r'[^\d.]', '', fraction_data.adv_impo)
if clean:
ad_valorem = float(clean)
except:
pass
us_dto = USTariffFractionCreateDTO(
code=fraction_data.fraction, # Store the punctuated fraction in the DB
description=fraction_data.description,
unit_of_measure=fraction_data.umt,
ad_valorem=ad_valorem,
# Defaults for others
prefix=None,
type_code=None,
fixed_cost=None
)
created = USTariffFractionService.create(db, us_dto, tenant_id, company_id)
return TariffFractionService.to_domain_usa_local(created)
else:
raise HTTPException(status_code=400, detail=f"Creation not allowed for '{catalog}' catalog (Read-Only)")
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail=f"Creation not allowed for '{catalog}' catalog (read-only; US data from SITAR).",
@@ -120,6 +171,39 @@ async def update_tariff_fraction(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
from fastapi import HTTPException
tenant_id = current_user.get("tenant_id")
if catalog == "american":
validate_access_to_resource(db, company_id, current_user, ["frac_american.edit"])
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.service import USTariffFractionService
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.dto import USTariffFractionUpdateDTO
import re
ad_valorem = None
if fraction_data.adv_impo:
try:
clean = re.sub(r'[^\d.]', '', fraction_data.adv_impo)
if clean:
ad_valorem = float(clean)
except:
pass
us_dto = USTariffFractionUpdateDTO(
code=fraction_data.fraction,
description=fraction_data.description,
unit_of_measure=fraction_data.umt,
ad_valorem=ad_valorem
)
updated = USTariffFractionService.update(db, tariff_fraction_id, tenant_id, us_dto, company_id)
if not updated:
raise HTTPException(status_code=404, detail="US Tariff fraction not found")
return TariffFractionService.to_domain_usa_local(updated)
else:
raise HTTPException(status_code=400, detail=f"Update not allowed for '{catalog}' catalog (Read-Only)")
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail=f"Update not allowed for '{catalog}' catalog (read-only; US data from SITAR).",
@@ -138,6 +222,19 @@ async def delete_tariff_fraction(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
from fastapi import HTTPException
tenant_id = current_user.get("tenant_id")
if catalog == "american":
validate_access_to_resource(db, company_id, current_user, ["frac_american.delete"])
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.service import USTariffFractionService
success = USTariffFractionService.delete(db, tariff_fraction_id, tenant_id, company_id)
if not success:
raise HTTPException(status_code=404, detail="US Tariff fraction not found")
return {"ok": True}
else:
raise HTTPException(status_code=400, detail=f"Delete not allowed for '{catalog}' catalog (Read-Only)")
raise HTTPException(
status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
detail=f"Delete not allowed for '{catalog}' catalog (read-only; US data from SITAR).",

View File

@@ -76,6 +76,10 @@ async def list_us_tariff_fractions(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
from core.security import validate_access_to_resource as validate_perm
validate_perm(db, company_id, current_user, ["frac_american.view"])
tenant_id = validate_access_to_resource(db, company_id, current_user)
validate_access_to_resource(db, company_id, current_user)
skip = (page - 1) * page_size

View File

@@ -18,7 +18,12 @@ identifier_crud = TenantCRUDRoutes(
prefix="",
tags=["Identifiers"],
resource_name="Identifier",
enable_list=True
enable_list=True,
list_permissions=["cat_identifiers.view"],
get_permissions=["cat_identifiers.view"],
create_permissions=["cat_identifiers.create"],
update_permissions=["cat_identifiers.edit"],
delete_permissions=["cat_identifiers.delete"],
)
# Identifier Detail CRUD
@@ -29,7 +34,12 @@ detail_crud = TenantCRUDRoutes(
service=IdentifierDetailService,
prefix="/details",
tags=["Identifier Details"],
resource_name="Identifier Detail"
resource_name="Identifier Detail",
list_permissions=["cat_identifiers.view"],
get_permissions=["cat_identifiers.view"],
create_permissions=["cat_identifiers.create"],
update_permissions=["cat_identifiers.edit"],
delete_permissions=["cat_identifiers.delete"],
)
router.include_router(identifier_crud.router)

View File

@@ -13,4 +13,10 @@ router = TenantCRUDRoutes(
tags=["a76.general_catalogs.inpc"],
resource_name="INPC",
enable_list=True,
# Permisos
list_permissions=["cat_inpc.view"],
get_permissions=["cat_inpc.view"],
create_permissions=["cat_inpc.create"],
update_permissions=["cat_inpc.edit"],
delete_permissions=["cat_inpc.delete"],
).router

View File

@@ -13,4 +13,9 @@ router = TenantCRUDRoutes(
tags=["a76.general_catalogs.legends"],
resource_name="Legend",
enable_list=True,
list_permissions=["cat_legends.view"],
get_permissions=["cat_legends.view"],
create_permissions=["cat_legends.create"],
update_permissions=["cat_legends.edit"],
delete_permissions=["cat_legends.delete"],
).router

View File

@@ -16,6 +16,11 @@ multi_currency_type_crud = TenantCRUDRoutes(
tags=["Multi Currency Types"],
resource_name="MultiCurrencyType",
enable_list=True,
list_permissions=["cat_multi_currency_types.view"],
get_permissions=["cat_multi_currency_types.view"],
create_permissions=["cat_multi_currency_types.create"],
update_permissions=["cat_multi_currency_types.edit"],
delete_permissions=["cat_multi_currency_types.delete"],
)
router.include_router(multi_currency_type_crud.router)

View File

@@ -21,4 +21,10 @@ router = TenantCRUDRoutes(
enable_filters=True, # Enable filtering by key and description_es
default_page_size=50,
max_page_size=100,
# Permisos
list_permissions=["cat_packages.view"],
get_permissions=["cat_packages.view"],
create_permissions=["cat_packages.create"],
update_permissions=["cat_packages.edit"],
delete_permissions=["cat_packages.delete"],
).router

View File

@@ -14,4 +14,9 @@ router = TenantCRUDRoutes(
resource_name="Port",
enable_list=True,
max_page_size=1000,
list_permissions=["cat_ports.view"],
get_permissions=["cat_ports.view"],
create_permissions=["cat_ports.create"],
update_permissions=["cat_ports.edit"],
delete_permissions=["cat_ports.delete"],
).router

View File

@@ -21,4 +21,9 @@ router = TenantCRUDRoutes(
enable_filters=True, # Enable filtering by seal
default_page_size=50,
max_page_size=100,
list_permissions=["cat_seals.view"],
get_permissions=["cat_seals.view"],
create_permissions=["cat_seals.create"],
update_permissions=["cat_seals.edit"],
delete_permissions=["cat_seals.delete"],
).router

View File

@@ -20,4 +20,9 @@ router = TenantCRUDRoutes(
enable_filters=True,
default_page_size=50,
max_page_size=100,
list_permissions=["cat_sectors.view"],
get_permissions=["cat_sectors.view"],
create_permissions=["cat_sectors.create"],
update_permissions=["cat_sectors.edit"],
delete_permissions=["cat_sectors.delete"],
).router

View File

@@ -13,4 +13,9 @@ router = TenantCRUDRoutes(
id_name="id",
enable_list=True,
enable_filters=True,
list_permissions=["cat_unit_conversions.view"],
get_permissions=["cat_unit_conversions.view"],
create_permissions=["cat_unit_conversions.create"],
update_permissions=["cat_unit_conversions.edit"],
delete_permissions=["cat_unit_conversions.delete"],
).router

View File

@@ -18,6 +18,12 @@ ace_router = TenantCRUDRoutes(
enable_list=True,
enable_filters=True,
max_page_size=10000,
# Permisos
list_permissions=["cat_um_ace.view"],
get_permissions=["cat_um_ace.view"],
create_permissions=["cat_um_ace.create"],
update_permissions=["cat_um_ace.edit"],
delete_permissions=["cat_um_ace.delete"],
).router
router.include_router(ace_router)
@@ -34,6 +40,12 @@ oma_router = TenantCRUDRoutes(
enable_list=True,
enable_filters=True,
max_page_size=10000,
# Permisos
list_permissions=["cat_um_oma.view"],
get_permissions=["cat_um_oma.view"],
create_permissions=["cat_um_oma.create"],
update_permissions=["cat_um_oma.edit"],
delete_permissions=["cat_um_oma.delete"],
).router
router.include_router(oma_router)
@@ -50,6 +62,12 @@ american_router = TenantCRUDRoutes(
enable_list=True,
enable_filters=True,
max_page_size=10000,
# Permisos
list_permissions=["cat_um_american.view"],
get_permissions=["cat_um_american.view"],
create_permissions=["cat_um_american.create"],
update_permissions=["cat_um_american.edit"],
delete_permissions=["cat_um_american.delete"],
).router
router.include_router(american_router)
@@ -66,6 +84,12 @@ customs_router = TenantCRUDRoutes(
enable_list=True,
enable_filters=True,
max_page_size=10000,
# Permisos
list_permissions=["cat_um_customs.view"],
get_permissions=["cat_um_customs.view"],
create_permissions=["cat_um_customs.create"],
update_permissions=["cat_um_customs.edit"],
delete_permissions=["cat_um_customs.delete"],
).router
router.include_router(customs_router)
@@ -82,6 +106,12 @@ general_router = TenantCRUDRoutes(
enable_list=True,
enable_filters=True,
max_page_size=10000,
# Permisos
list_permissions=["cat_um_general.view"],
get_permissions=["cat_um_general.view"],
create_permissions=["cat_um_general.create"],
update_permissions=["cat_um_general.edit"],
delete_permissions=["cat_um_general.delete"],
).router
router.include_router(general_router)
@@ -99,5 +129,11 @@ main_router = TenantCRUDRoutes(
enable_list=True,
enable_filters=True,
max_page_size=10000,
# Permisos
list_permissions=["cat_um_general.view"],
get_permissions=["cat_um_general.view"],
create_permissions=["cat_um_general.create"],
update_permissions=["cat_um_general.edit"],
delete_permissions=["cat_um_general.delete"],
).router
router.include_router(main_router)

View File

@@ -5,12 +5,63 @@ from core.database import get_core_db
from core.security import get_current_user, validate_access_to_resource
from api.v1.modules.a76.invoice_settings import services
from api.v1.modules.a76.invoice_settings.dto import InvoiceSettingsRequest, InvoiceSettingsResponse, OperationType
from api.v1.modules.core.permissions.service import PermissionService
router = APIRouter(
prefix="/a76/invoice-settings",
tags=["a76/invoice-settings"]
)
def _invoice_perm_base_for_settings(operation_type: OperationType, invoice_type: str) -> str:
"""Alineado con get_invoice_permission_base en invoices/routes (defaults por tipo)."""
op = (
operation_type.value
if hasattr(operation_type, "value")
else str(operation_type).lower().split(".")[-1]
)
inv = (invoice_type or "").upper()
if op == "imp":
if inv == "TEM":
return "invoice.imp.tem"
if inv == "DEF":
return "invoice.imp.def"
if inv == "MEX":
return "invoice.imp.cm"
if inv == "CR":
return "invoice.imp.cr"
return "invoice.imp.tem"
if op == "exp":
if inv == "REPAR":
return "invoice.exp.rep"
return "invoice.exp"
return "invoice.imp.tem"
def _can_read_invoice_settings_row(
db: Session,
company_id: int,
current_user: Dict[str, Any],
invoice_type: str,
operation_type: OperationType,
) -> bool:
"""
Ver configuración por tipo/op: settings_general.view O ver facturas de ese mismo contexto
(para cargar defaults en alta/edición sin abrir la pantalla de parámetros).
"""
user_roles = current_user.get("realm_access", {}).get("roles", [])
if "admin" in user_roles:
return True
user_id = current_user.get("sub") or current_user.get("id")
if not user_id:
return False
ps = PermissionService(db)
if ps.has_permission(str(user_id), company_id, "settings_general.view"):
return True
base = _invoice_perm_base_for_settings(operation_type, invoice_type)
return ps.has_permission(str(user_id), company_id, f"{base}.view")
@router.get("/{invoice_type}", response_model=InvoiceSettingsResponse)
def get_invoice_settings(
invoice_type: str,
@@ -21,6 +72,14 @@ def get_invoice_settings(
):
"""Get settings for a specific invoice type and operation"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
if not _can_read_invoice_settings_row(
db, company_id, current_user, invoice_type, operation_type
):
raise HTTPException(
status_code=403,
detail="Missing required permissions: settings_general.view "
f"(o permiso de vista del tipo de factura solicitado, p. ej. {_invoice_perm_base_for_settings(operation_type, invoice_type)}.view)",
)
settings = services.get_settings(
db,
@@ -49,7 +108,12 @@ def list_invoice_settings(
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""List all configured settings for validation or overview"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(
db,
company_id,
current_user,
required_permissions=["settings_general.view"],
)
return services.list_settings(
db,
@@ -65,7 +129,12 @@ def save_invoice_settings(
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Create or update invoice settings"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(
db,
company_id,
current_user,
required_permissions=["settings_general.edit"],
)
return services.upsert_settings(
db,

View File

@@ -9,6 +9,7 @@ from core.security import get_current_user, validate_access_to_resource
from api.v1.modules.core.tasks_tracking import track_and_dispatch
from api.v1.modules.a76.invoices.models import InvoiceHeader, OperationType
from api.v1.modules.a76.invoices.routes import get_invoice_permission_base
from .task import process_invoice_task
from ...exports.process.task import process_export_invoice_task
@@ -34,6 +35,10 @@ def trigger_invoice_process(
if invoice is None:
raise HTTPException(status_code=404, detail=f"Factura {invoice_id} no encontrada.")
# 🛡️ Validación de permisos granulares
perm_base = get_invoice_permission_base(invoice.operation_type, invoice.invoice_type)
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.process"])
if invoice.operation_type == OperationType.EXP:
task = track_and_dispatch(
db=db,

View File

@@ -9,6 +9,7 @@ from core.security import get_current_user, validate_access_to_resource
from api.v1.modules.core.tasks_tracking import track_and_dispatch
from api.v1.modules.a76.invoices.models import InvoiceHeader, OperationType
from api.v1.modules.a76.invoices.routes import get_invoice_permission_base
from .task import revert_invoice_task as revert_import_invoice_task
from ...exports.revert.task import revert_invoice_task as revert_export_invoice_task
@@ -40,6 +41,10 @@ def trigger_invoice_revert(
if invoice is None:
raise HTTPException(status_code=404, detail=f"Factura {invoice_id} no encontrada.")
# 🛡️ Validación de permisos granulares: des-actualizar requiere el mismo permiso que procesar
perm_base = get_invoice_permission_base(invoice.operation_type, invoice.invoice_type)
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.process"])
if invoice.operation_type == OperationType.EXP:
task = track_and_dispatch(
db=db,

View File

@@ -773,4 +773,3 @@ class InvoiceCollections(Base, TenantScopedMixin, TimestampMixin):
# Relationship
header: Mapped["InvoiceHeader"] = relationship(back_populates="collections")
concept: Mapped[Optional[str]] = mapped_column(String(100)) # CONCEPTO / Conce

View File

@@ -1,9 +1,8 @@
from typing import Dict, Any, Optional
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from core.database import get_core_db
from core.security import get_current_user, validate_access_to_resource
from fastapi import APIRouter, Depends, HTTPException, Query, Path
from sqlalchemy import func
from sqlalchemy import func, or_, and_
from sqlalchemy.orm import Session
from . import schemas, services, models
@@ -12,6 +11,28 @@ from .catalog_service import InvoiceCatalogService
# Create main router
router = APIRouter()
# --- 🛡️ FUNCIÓN EVALUADORA DE PERMISOS DINÁMICOS ---
def get_invoice_permission_base(operation_type: Any, invoice_type: Any) -> str:
"""Devuelve la clave base del permiso dependiendo del tipo de factura"""
# Limpiamos los valores por si vienen como Enums
op = str(operation_type).lower().split('.')[-1] if operation_type else ''
inv = str(invoice_type).upper() if invoice_type else ''
if op == 'imp':
if inv == 'TEM': return 'invoice.imp.tem'
if inv == 'DEF': return 'invoice.imp.def'
if inv == 'MEX': return 'invoice.imp.cm'
if inv == 'CR': return 'invoice.imp.cr'
return 'invoice.imp.tem' # Fallback
elif op == 'exp':
if inv == 'REPAR': return 'invoice.exp.rep'
return 'invoice.exp'
return 'invoice.imp.tem' # Fallback general
# --- RUTAS DE UTILIDAD ---
@router.get("/invoices/creation-data", response_model=schemas.InvoiceCreationResponse)
def get_creation_data(
company_id: int = Query(..., description="Company ID"),
@@ -19,8 +40,14 @@ def get_creation_data(
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Get consolidated data for creating a new invoice"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
return InvoiceCatalogService.get_creation_data(db, tenant_id, company_id)
try:
tenant_id = validate_access_to_resource(db, company_id, current_user)
return InvoiceCatalogService.get_creation_data(db, tenant_id, company_id)
except Exception as e:
import traceback
print(f"[ERROR] get_creation_data failed: {str(e)}")
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Error al cargar datos de creación: {str(e)}")
@router.get("/invoices/{invoice_id}/edition-data", response_model=schemas.InvoiceEditionResponse)
def get_edition_data(
@@ -30,12 +57,27 @@ def get_edition_data(
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Get consolidated data for editing an existing invoice"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
data = InvoiceCatalogService.get_edition_data(db, invoice_id, tenant_id, company_id)
if not data:
raise HTTPException(status_code=404, detail="Invoice not found")
return data
try:
tenant_id = validate_access_to_resource(db, company_id, current_user)
# 1. Traer la factura para saber su tipo
invoice = services.InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
if not invoice:
raise HTTPException(status_code=404, detail="Invoice not found")
# 2. Validar permiso dinámico de Edición
perm_base = get_invoice_permission_base(invoice.operation_type, invoice.invoice_type)
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.edit"])
data = InvoiceCatalogService.get_edition_data(db, invoice_id, tenant_id, company_id)
return data
except HTTPException:
raise
except Exception as e:
import traceback
print(f"[ERROR] get_edition_data failed: {str(e)}")
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Error al cargar datos de edición: {str(e)}")
@router.get("/invoices/remesa-suggestion", response_model=Dict[str, int])
def get_remesa_suggestion(
@@ -44,7 +86,6 @@ def get_remesa_suggestion(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Suggest next remesa for selected pedimento (max+1)."""
tenant_id = validate_access_to_resource(db, company_id, current_user)
max_rem = (
db.query(func.max(models.InvoiceComplianceMx.remesa))
@@ -58,31 +99,84 @@ def get_remesa_suggestion(
return {"next_remesa": int((max_rem or 0) + 1)}
# Create CRUD routes for Invoice Header using TenantCRUDRoutes
invoice_crud = TenantCRUDRoutes(
service=services.InvoiceService,
create_schema=schemas.InvoiceHeaderCreate,
update_schema=schemas.InvoiceHeaderUpdate,
response_schema=schemas.InvoiceHeaderResponse,
prefix="/invoices",
tags=[],
resource_name="Invoice",
id_name="invoice_id",
id_type=int,
enable_list=False, # Disable auto-list to override with custom filter
enable_filters=True, # Enable filters for status, operation_type, etc.
list_permissions=[],
get_permissions=[],
create_permissions=[],
update_permissions=[],
delete_permissions=[],
default_page_size=50,
max_page_size=200,
)
# --- RUTAS CRUD MANUALES (Sustituyen al TenantCRUDRoutes por seguridad) ---
# Include the main CRUD routes
router.include_router(invoice_crud.router)
@router.post("/invoices/", response_model=schemas.InvoiceHeaderResponse)
def create_invoice(
data: schemas.InvoiceHeaderCreate,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
try:
tenant_id = validate_access_to_resource(db, company_id, current_user)
# Validamos usando los datos que vienen en el body (payload)
perm_base = get_invoice_permission_base(data.operation_type, data.invoice_type)
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.create"])
return services.InvoiceService.create(db, data, tenant_id, company_id)
except Exception as e:
import traceback
print(f"[ERROR] create_invoice failed: {str(e)}")
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Error al guardar factura: {str(e)}")
@router.get("/invoices/{invoice_id}", response_model=schemas.InvoiceHeaderResponse)
def get_invoice(
invoice_id: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
invoice = services.InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
if not invoice:
raise HTTPException(status_code=404, detail="Invoice not found")
perm_base = get_invoice_permission_base(invoice.operation_type, invoice.invoice_type)
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.view"])
return invoice
@router.put("/invoices/{invoice_id}", response_model=schemas.InvoiceHeaderResponse)
def update_invoice(
invoice_id: int,
data: schemas.InvoiceHeaderUpdate,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
invoice = services.InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
if not invoice:
raise HTTPException(status_code=404, detail="Invoice not found")
perm_base = get_invoice_permission_base(invoice.operation_type, invoice.invoice_type)
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.edit"])
return services.InvoiceService.update(db, invoice_id, tenant_id, data, company_id)
@router.delete("/invoices/{invoice_id}")
def delete_invoice(
invoice_id: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
invoice = services.InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
if not invoice:
raise HTTPException(status_code=404, detail="Invoice not found")
perm_base = get_invoice_permission_base(invoice.operation_type, invoice.invoice_type)
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.delete"])
success = services.InvoiceService.delete(db, invoice_id, tenant_id, company_id)
return {"success": success}
# --- RUTA DE LISTADO (FILTROS) ---
@router.get("/invoices/", response_model=schemas.InvoiceHeaderListResponse)
def list_invoices(
@@ -104,101 +198,133 @@ def list_invoices(
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""
List invoices with optional filters, including manifest_number.
List invoices with optional filters and granular permission enforcement.
"""
print(f"DEBUG: list_invoices called with manifest_number={manifest_number}")
tenant_id = validate_access_to_resource(db, company_id, current_user)
print(f"DEBUG: tenant_id={tenant_id}, company_id={company_id}")
user_roles = current_user.get("realm_access", {}).get("roles", [])
allowed_filters = []
# Información del usuario para debugging (se ve en los logs del servidor)
user_name = current_user.get('preferred_username') or current_user.get('email', 'Desconocido')
skip = (page - 1) * page_size
filters = {
"invoice_number": invoice_number or search,
"status": status,
"operation_type": operation_type,
"invoice_type": invoice_type,
"manifest_number": manifest_number,
"pedimento": pedimento,
"project_number": project_number,
"year": year,
}
from api.v1.modules.core.permissions.service import PermissionService
user_id = current_user.get("sub") or current_user.get("id")
perm_service = PermissionService(db)
perm_codes = perm_service.get_user_permissions(user_id, company_id)
# Remove None values
filters = {k: v for k, v in filters.items() if v is not None}
# 🕵️ DEBUG LOGS - Cruciales para diagnosticar filtrado que no funciona
print(f"[AUTH] User: {user_name} (ID: {user_id})")
print(f"[AUTH] App Permissions (C{company_id}): {perm_codes}")
items, total = services.InvoiceService.get_all(
db, tenant_id, company_id, skip=skip, limit=page_size, filters=filters, sort_by=sort_by, sort_order=sort_order
)
print(f"DEBUG: InvoiceService returned {len(items)} items, total={total}")
# Definimos si debe saltar el filtrado granular (SOLO con permiso explícito)
has_global_view = "invoice.view_all" in perm_codes
return {
"items": items,
"total": total,
"page": page,
"page_size": page_size
}
# El rol de admin de Keycloak ya NO otorga bypass automático si hay permisos granulares
if has_global_view:
print(f"[AUTH] GLOBAL ACCESS for {user_name}")
allowed_filters = None
else:
# Aplicamos filtros basados en permisos específicos
# Importaciones
if "invoice.imp.tem.view" in perm_codes: allowed_filters.append(("imp", "TEM"))
if "invoice.imp.def.view" in perm_codes: allowed_filters.append(("imp", "DEF"))
if "invoice.imp.cm.view" in perm_codes: allowed_filters.append(("imp", "MEX"))
if "invoice.imp.cr.view" in perm_codes: allowed_filters.append(("imp", "CR"))
if "invoice.imp.rep.view" in perm_codes: allowed_filters.append(("imp", "REP"))
# Exportaciones
if "invoice.exp.rep.view" in perm_codes: allowed_filters.append(("exp", "REPAR"))
if "invoice.exp.donac.view" in perm_codes: allowed_filters.append(("exp", "DONAC"))
# Permiso general de exportación
if "invoice.exp.view" in perm_codes:
for t in ["EXDEF", "MATDE", "NODES", "PTERM", "SCRAP", "VEMEX", "VIRTU", "AFIJO", "REEXP"]:
if ("exp", t) not in allowed_filters:
allowed_filters.append(("exp", t))
print(f"[AUTH] Filtered access for {user_name}. Allowed types count: {len(allowed_filters)}")
if not allowed_filters:
# Si no tiene ningún permiso de factura, bloqueamos
# Excepto si es un admin de Keycloak, le damos el beneficio de la duda pero logeamos
if "admin" in user_roles:
print(f"[AUTH] Keycloak Admin {user_name} has no app permissions. Granting view_all as fallback.")
allowed_filters = None
else:
raise HTTPException(status_code=403, detail="No tienes permisos para ver facturas en esta empresa")
try:
skip = (page - 1) * page_size
# Combinar filtros de búsqueda con filtros granulares de permisos
filters = {
"invoice_number": invoice_number,
"status": status,
"operation_type": operation_type.value if operation_type else None,
"invoice_type": invoice_type,
"manifest_number": manifest_number,
"pedimento": pedimento,
"project_number": project_number,
"year": year,
"allowed_types": allowed_filters
}
filters = {k: v for k, v in filters.items() if v is not None}
items, total = services.InvoiceService.get_all(
db, tenant_id, company_id, skip=skip, limit=page_size, filters=filters, sort_by=sort_by, sort_order=sort_order
)
return {
"items": items,
"total": total,
"page": page,
"page_size": page_size
}
except Exception as e:
import traceback
print(f"[ERROR] list_invoices failed: {str(e)}")
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Internal server error in invoices list: {str(e)}")
# Additional nested routes for child resources
# --- RUTAS DE LOGÍSTICA ---
# --- Logistics Routes ---
@router.get(
"/invoices/{invoice_id}/logistics",
response_model=list[schemas.InvoiceLogisticsResponse],
summary="Get all logistics for an invoice",
)
@router.get("/invoices/{invoice_id}/logistics", response_model=list[schemas.InvoiceLogisticsResponse])
def get_invoice_logistics(
invoice_id: int = Path(..., description="Invoice ID"),
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Get all logistics entries for a specific invoice"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
invoice = services.InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
if not invoice: raise HTTPException(status_code=404, detail="Invoice not found")
perm_base = get_invoice_permission_base(invoice.operation_type, invoice.invoice_type)
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.view"])
# Verify the invoice exists and belongs to the tenant/company
invoice = services.InvoiceService.get_by_id(
db, invoice_id, tenant_id, company_id)
if not invoice:
raise HTTPException(status_code=404, detail="Invoice not found")
return services.InvoiceLogisticsService.get_all_by_invoice(db, invoice_id)
logistics = services.InvoiceLogisticsService.get_all_by_invoice(
db, invoice_id)
return logistics
@router.post(
"/invoices/{invoice_id}/logistics",
response_model=schemas.InvoiceLogisticsResponse,
status_code=201,
summary="Add logistics to an invoice",
)
@router.post("/invoices/{invoice_id}/logistics", response_model=schemas.InvoiceLogisticsResponse, status_code=201)
def create_invoice_logistics(
logistics_data: schemas.InvoiceLogisticsCreate,
invoice_id: int = Path(..., description="Invoice ID"),
logistics_data: schemas.InvoiceLogisticsCreate = ...,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Add a new logistics entry to an invoice"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
invoice = services.InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
if not invoice: raise HTTPException(status_code=404, detail="Invoice not found")
# Editar los hijos cuenta como editar la factura padre
perm_base = get_invoice_permission_base(invoice.operation_type, invoice.invoice_type)
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.edit"])
# Verify the invoice exists and belongs to the tenant/company
invoice = services.InvoiceService.get_by_id(
db, invoice_id, tenant_id, company_id)
if not invoice:
raise HTTPException(status_code=404, detail="Invoice not found")
return services.InvoiceLogisticsService.create(db, logistics_data, invoice_id, tenant_id, company_id)
logistics = services.InvoiceLogisticsService.create(
db, logistics_data, invoice_id, tenant_id, company_id)
return logistics
@router.delete(
"/invoices/{invoice_id}/logistics/{logistics_id}",
status_code=204,
summary="Delete logistics from an invoice",
)
@router.delete("/invoices/{invoice_id}/logistics/{logistics_id}", status_code=204)
def delete_invoice_logistics(
invoice_id: int = Path(..., description="Invoice ID"),
logistics_id: int = Path(..., description="Logistics ID"),
@@ -206,84 +332,54 @@ def delete_invoice_logistics(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Delete a logistics entry from an invoice"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
invoice = services.InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
if not invoice: raise HTTPException(status_code=404, detail="Invoice not found")
perm_base = get_invoice_permission_base(invoice.operation_type, invoice.invoice_type)
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.edit"])
# Verify the invoice exists and belongs to the tenant/company
invoice = services.InvoiceService.get_by_id(
db, invoice_id, tenant_id, company_id)
if not invoice:
raise HTTPException(status_code=404, detail="Invoice not found")
success = services.InvoiceLogisticsService.delete(
db, logistics_id, invoice_id)
if not success:
raise HTTPException(
status_code=404, detail="Logistics entry not found")
if not services.InvoiceLogisticsService.delete(db, logistics_id, invoice_id):
raise HTTPException(status_code=404, detail="Logistics entry not found")
return None
# --- Sales Details Routes ---
# --- RUTAS DE DETALLES DE VENTA (PARTIDAS) ---
@router.get(
"/invoices/{invoice_id}/details",
response_model=list[schemas.InvoiceSalesDetailsResponse],
summary="Get all sales details for an invoice",
)
@router.get("/invoices/{invoice_id}/details", response_model=list[schemas.InvoiceSalesDetailsResponse])
def get_invoice_details(
invoice_id: int = Path(..., description="Invoice ID"),
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Get all sales details for a specific invoice"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
invoice = services.InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
if not invoice: raise HTTPException(status_code=404, detail="Invoice not found")
perm_base = get_invoice_permission_base(invoice.operation_type, invoice.invoice_type)
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.view"])
# Verify the invoice exists and belongs to the tenant/company
invoice = services.InvoiceService.get_by_id(
db, invoice_id, tenant_id, company_id)
if not invoice:
raise HTTPException(status_code=404, detail="Invoice not found")
details = services.InvoiceSalesDetailsService.get_all_by_invoice(
db, invoice_id)
return details
@router.post(
"/invoices/{invoice_id}/details",
response_model=schemas.InvoiceSalesDetailsResponse,
status_code=201,
summary="Add sales detail to an invoice",
)
return services.InvoiceSalesDetailsService.get_all_by_invoice(db, invoice_id)
@router.post("/invoices/{invoice_id}/details", response_model=schemas.InvoiceSalesDetailsResponse, status_code=201)
def create_invoice_detail(
detail_data: schemas.InvoiceSalesDetailsCreate,
invoice_id: int = Path(..., description="Invoice ID"),
detail_data: schemas.InvoiceSalesDetailsCreate = ...,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Add a new sales detail to an invoice"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
invoice = services.InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
if not invoice: raise HTTPException(status_code=404, detail="Invoice not found")
perm_base = get_invoice_permission_base(invoice.operation_type, invoice.invoice_type)
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.edit"])
# Verify the invoice exists and belongs to the tenant/company
invoice = services.InvoiceService.get_by_id(
db, invoice_id, tenant_id, company_id)
if not invoice:
raise HTTPException(status_code=404, detail="Invoice not found")
return services.InvoiceSalesDetailsService.create(db, detail_data, invoice_id, tenant_id, company_id)
detail = services.InvoiceSalesDetailsService.create(
db, detail_data, invoice_id, tenant_id, company_id)
return detail
@router.delete(
"/invoices/{invoice_id}/details/{detail_id}",
status_code=204,
summary="Delete sales detail from an invoice",
)
@router.delete("/invoices/{invoice_id}/details/{detail_id}", status_code=204)
def delete_invoice_detail(
invoice_id: int = Path(..., description="Invoice ID"),
detail_id: int = Path(..., description="Detail ID"),
@@ -291,82 +387,54 @@ def delete_invoice_detail(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Delete a sales detail from an invoice"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
invoice = services.InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
if not invoice: raise HTTPException(status_code=404, detail="Invoice not found")
perm_base = get_invoice_permission_base(invoice.operation_type, invoice.invoice_type)
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.edit"])
# Verify the invoice exists and belongs to the tenant/company
invoice = services.InvoiceService.get_by_id(
db, invoice_id, tenant_id, company_id)
if not invoice:
raise HTTPException(status_code=404, detail="Invoice not found")
success = services.InvoiceSalesDetailsService.delete(
db, detail_id, invoice_id)
if not success:
if not services.InvoiceSalesDetailsService.delete(db, detail_id, invoice_id):
raise HTTPException(status_code=404, detail="Sales detail not found")
return None
# --- Collections Routes ---
# --- RUTAS DE COBRANZA (COLLECTIONS) ---
@router.get(
"/invoices/{invoice_id}/collections",
response_model=list[schemas.InvoiceCollectionsResponse],
summary="Get all collections for an invoice",
)
@router.get("/invoices/{invoice_id}/collections", response_model=list[schemas.InvoiceCollectionsResponse])
def get_invoice_collections(
invoice_id: int = Path(..., description="Invoice ID"),
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Get all collections for a specific invoice"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
invoice = services.InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
if not invoice: raise HTTPException(status_code=404, detail="Invoice not found")
perm_base = get_invoice_permission_base(invoice.operation_type, invoice.invoice_type)
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.view"])
# Verify the invoice exists and belongs to the tenant/company
invoice = services.InvoiceService.get_by_id(
db, invoice_id, tenant_id, company_id)
if not invoice:
raise HTTPException(status_code=404, detail="Invoice not found")
return services.InvoiceCollectionsService.get_all_by_invoice(db, invoice_id)
collections = services.InvoiceCollectionsService.get_all_by_invoice(
db, invoice_id)
return collections
@router.post(
"/invoices/{invoice_id}/collections",
response_model=schemas.InvoiceCollectionsResponse,
status_code=201,
summary="Add collection to an invoice",
)
@router.post("/invoices/{invoice_id}/collections", response_model=schemas.InvoiceCollectionsResponse, status_code=201)
def create_invoice_collection(
collection_data: schemas.InvoiceCollectionsCreate,
invoice_id: int = Path(..., description="Invoice ID"),
collection_data: schemas.InvoiceCollectionsCreate = ...,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Add a new collection to an invoice"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
invoice = services.InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
if not invoice: raise HTTPException(status_code=404, detail="Invoice not found")
perm_base = get_invoice_permission_base(invoice.operation_type, invoice.invoice_type)
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.edit"])
# Verify the invoice exists and belongs to the tenant/company
invoice = services.InvoiceService.get_by_id(
db, invoice_id, tenant_id, company_id)
if not invoice:
raise HTTPException(status_code=404, detail="Invoice not found")
return services.InvoiceCollectionsService.create(db, collection_data, invoice_id, tenant_id, company_id)
collection = services.InvoiceCollectionsService.create(
db, collection_data, invoice_id, tenant_id, company_id)
return collection
@router.delete(
"/invoices/{invoice_id}/collections/{collection_id}",
status_code=204,
summary="Delete collection from an invoice",
)
@router.delete("/invoices/{invoice_id}/collections/{collection_id}", status_code=204)
def delete_invoice_collection(
invoice_id: int = Path(..., description="Invoice ID"),
collection_id: int = Path(..., description="Collection ID"),
@@ -374,18 +442,13 @@ def delete_invoice_collection(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""Delete a collection from an invoice"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
invoice = services.InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
if not invoice: raise HTTPException(status_code=404, detail="Invoice not found")
perm_base = get_invoice_permission_base(invoice.operation_type, invoice.invoice_type)
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.edit"])
# Verify the invoice exists and belongs to the tenant/company
invoice = services.InvoiceService.get_by_id(
db, invoice_id, tenant_id, company_id)
if not invoice:
raise HTTPException(status_code=404, detail="Invoice not found")
success = services.InvoiceCollectionsService.delete(
db, collection_id, invoice_id)
if not success:
if not services.InvoiceCollectionsService.delete(db, collection_id, invoice_id):
raise HTTPException(status_code=404, detail="Collection not found")
return None
return None

View File

@@ -326,6 +326,35 @@ class InvoiceService:
if not filters.get("invoice_type") and ot_exp_val == "exp":
query = query.filter(models.InvoiceHeader.invoice_type != "REPAR")
# Filtro por permisos granulares (allowed_types)
if "allowed_types" in filters:
from sqlalchemy import or_, and_
allowed = filters["allowed_types"]
if allowed is None:
# Acceso global (admin o view_all) - no filtramos por tipos
pass
elif not allowed:
# Seguridad: Si el usuario NO tiene permisos para ningún tipo específico
query = query.filter(models.InvoiceHeader.id == -1)
else:
from sqlalchemy import func
conditions = []
for op, inv in allowed:
# Aseguramos comparación insensible a mayúsculas para mayor robustez con la DB
op_str = str(op).lower()
inv_str = str(inv).lower()
conditions.append(
and_(
func.lower(models.InvoiceHeader.operation_type) == op_str,
func.lower(models.InvoiceHeader.invoice_type) == inv_str
)
)
if conditions:
query = query.filter(or_(*conditions))
else:
# Seguridad: Si tiene allowed_types pero no generamos condiciones, no debe ver nada
query = query.filter(models.InvoiceHeader.id == -1)
# Apply sorting
if sort_by:
# Simple column mapping

View File

@@ -22,9 +22,7 @@ from .service import ItemService
router = APIRouter(prefix="/items", tags=["Items"])
# ============================================================================
# ITEM CRUD ENDPOINTS
# ============================================================================
@router.post("/", response_model=LineItemResponse, status_code=status.HTTP_201_CREATED)
async def create_item(
@@ -186,9 +184,7 @@ async def delete_item_series(
return {"message": f"Successfully deleted {count} series", "count": count}
# ============================================================================
# ADDITIONAL ENDPOINTS FOR INVOICE
# ============================================================================
@router.get("/invoice/{invoice_id}/items/", response_model=LineItemListResponse)
async def list_items_by_invoice(
@@ -256,9 +252,7 @@ async def get_items_with_balance(
return service.get_lines_with_balance(db, invoice_id, tenant_id, company_id, as_of_date, current_export_invoice_id)
# ============================================================================
# STATISTICS & UTILITIES
# ============================================================================
@router.get("/stats/summary")

View File

@@ -47,7 +47,7 @@ async def upload_import_file(
current_user: Dict[str, Any] = Depends(get_current_user),
):
try:
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["csv_upload.process"])
except Exception as e:
logger.error(f"BOMs import: access validation failed: {e}")
raise HTTPException(status_code=403, detail="Invalid company access")

View File

@@ -52,7 +52,7 @@ async def upload_import_file(
):
"""Subir CSV, guardar en Redis, encolar scan. template_id/document_type distinguen Cambio de régimen vs Regularización."""
try:
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["csv_upload.process"])
except Exception as e:
logger.error("Cambio régimen/Regularización import: access validation failed: %s", e)
raise HTTPException(status_code=403, detail="Invalid company access")

View File

@@ -49,7 +49,7 @@ async def upload_import_file(
current_user: Dict[str, Any] = Depends(get_current_user),
):
try:
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["csv_upload.process"])
except Exception as e:
logger.error(f"Classes import: access validation failed: {e}")
raise HTTPException(status_code=403, detail="Invalid company access")

View File

@@ -50,10 +50,10 @@ async def upload_import_file(
Fase 1: Subir CSV, guardar en Redis, encolar tarea de escaneo.
"""
try:
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["csv_upload.process"])
except Exception as e:
logger.error(f"CP import: access validation failed: {e}")
raise HTTPException(status_code=403, detail="Invalid company access")
raise HTTPException(status_code=403, detail="Invalid company access or missing permissions")
if not file.filename or not file.filename.lower().endswith(".csv"):
raise HTTPException(status_code=400, detail="Solo se permiten archivos .csv")
@@ -166,6 +166,7 @@ async def commit_import_job(
"""
Fase 2: Usuario confirma; se encola la inserción de filas válidas.
"""
validate_access_to_resource(db, company_id, current_user, ["csv_upload.process"]) # company_id will be extracted from Redis meta
r = _get_redis()
commit_id = dispatch_tracked_layouts_csv_commit(
db=db,

View File

@@ -50,10 +50,10 @@ async def upload_import_file(
Fase 1: Subir CSV, guardar en Redis, encolar tarea de escaneo.
"""
try:
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["csv_upload.process"])
except Exception as e:
logger.error(f"CB import: access validation failed: {e}")
raise HTTPException(status_code=403, detail="Invalid company access")
raise HTTPException(status_code=403, detail="Invalid company access or missing permissions")
if not file.filename or not file.filename.lower().endswith(".csv"):
raise HTTPException(status_code=400, detail="Solo se permiten archivos .csv")
@@ -166,6 +166,7 @@ async def commit_import_job(
"""
Fase 2: Usuario confirma; se encola la inserción de filas válidas.
"""
validate_access_to_resource(db, company_id, current_user, ["csv_upload.process"]) # company_id will be extracted from Redis meta
r = _get_redis()
commit_id = dispatch_tracked_layouts_csv_commit(
db=db,

View File

@@ -46,7 +46,7 @@ async def upload_import_file(
current_user: Dict[str, Any] = Depends(get_current_user),
):
try:
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["csv_upload.process"])
except Exception as e:
logger.error(f"Drivers import: access validation failed: {e}")
raise HTTPException(status_code=403, detail="Invalid company access")

View File

@@ -59,7 +59,7 @@ async def upload_import_file(
Parámetros globales de carga: reemplazar_sin_preguntar (Modo Reemplazar vs Actualizar), date_format (Formato de Fecha).
"""
try:
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["csv_upload.process"])
except Exception as e:
logger.error(f"ER import: access validation failed: {e}")
raise HTTPException(status_code=403, detail="Invalid company access")

View File

@@ -48,7 +48,7 @@ async def upload_import_file(
):
"""Subir CSV, guardar en Redis, encolar scan. operation_type=exp para exportación."""
try:
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["csv_upload.process"])
except Exception as e:
logger.error("Exportación import: access validation failed: %s", e)
raise HTTPException(status_code=403, detail="Invalid company access")

View File

@@ -55,7 +55,7 @@ async def upload_import_file(
"""
# 1. Validate Access & Get Tenant
try:
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["csv_upload.process"])
except Exception as e:
logger.error(f"Access validation failed: {e}")
raise HTTPException(status_code=403, detail="Invalid company access")

View File

@@ -49,7 +49,7 @@ async def upload_import_file(
current_user: Dict[str, Any] = Depends(get_current_user),
):
try:
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["csv_upload.process"])
except Exception as e:
logger.error(f"Parts import: access validation failed: {e}")
raise HTTPException(status_code=403, detail="Invalid company access")

View File

@@ -51,7 +51,7 @@ async def upload_import_file(
Fase 1: Subir CSV, guardar en Redis, encolar tarea de escaneo.
"""
try:
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["csv_upload.process"])
except Exception as e:
logger.error(f"Pedimentos import: access validation failed: {e}")
raise HTTPException(status_code=403, detail="Invalid company access")

View File

@@ -49,7 +49,7 @@ async def upload_import_file(
current_user: Dict[str, Any] = Depends(get_current_user),
):
try:
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["csv_upload.process"])
except Exception as e:
logger.error(f"Trailers import: access validation failed: {e}")
raise HTTPException(status_code=403, detail="Invalid company access")

View File

@@ -49,7 +49,7 @@ async def upload_import_file(
current_user: Dict[str, Any] = Depends(get_current_user),
):
try:
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["csv_upload.process"])
except Exception as e:
logger.error("Transportistas import: access validation failed: %s", e)
raise HTTPException(status_code=403, detail="Invalid company access")

View File

@@ -52,7 +52,7 @@ async def upload_import_file(
actualizar=True simula Clarion 'Agr./Actual.'; actualizar=False 'Agr./Reempl.'.
"""
try:
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["csv_upload.process"])
except Exception as e:
logger.error(f"FA import: access validation failed: {e}")
raise HTTPException(status_code=403, detail="Invalid company access")

View File

@@ -49,7 +49,7 @@ async def upload_import_file(
current_user: Dict[str, Any] = Depends(get_current_user),
):
try:
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["csv_upload.process"])
except Exception as e:
logger.error(f"Vehicles import: access validation failed: {e}")
raise HTTPException(status_code=403, detail="Invalid company access")

View File

@@ -32,7 +32,7 @@ async def list_manifests(
"""
List all manifests for a company with pagination and filters
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["export_manifest.view"])
filters = {
"search": search,
"manifest_number": manifest_number,
@@ -57,7 +57,7 @@ async def create_manifest(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["export_manifest.view"])
return ManifestService.create(db, manifest_data, tenant_id, company_id)
@@ -68,7 +68,7 @@ async def get_manifest(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["export_manifest.view"])
manifest = ManifestService.get_by_id(db, manifest_id, tenant_id, company_id)
if not manifest:
raise HTTPException(status_code=404, detail="Manifest not found")
@@ -83,7 +83,7 @@ async def update_manifest(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["export_manifest.view"])
manifest = ManifestService.update(
db, manifest_id, tenant_id, company_id, manifest_data
)
@@ -99,7 +99,7 @@ async def delete_manifest(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["export_manifest.view"])
success = ManifestService.delete(db, manifest_id, tenant_id, company_id)
if not success:
raise HTTPException(status_code=404, detail="Manifest not found")

View File

@@ -29,5 +29,9 @@ router.include_router(
enable_list=True,
enable_filters=True,
max_page_size=1000,
list_permissions=["goods_parts.view"],
create_permissions=["goods_parts.create"],
update_permissions=["goods_parts.edit"],
delete_permissions=["goods_parts.delete"],
).router
)

View File

@@ -29,6 +29,9 @@ async def get_creation_data(
Get all catalogs needed for creating a new pedimento.
Consolidates multiple catalog calls into a single endpoint.
"""
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["pedimentos_mgmt.view"])
tenant_id = current_user["tenant_id"]
try:
@@ -51,6 +54,9 @@ async def get_edition_data(
Get all catalogs and pedimento data needed for editing an existing pedimento.
Consolidates multiple catalog calls + pedimento fetch into a single endpoint.
"""
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["pedimentos_mgmt.view"])
tenant_id = current_user["tenant_id"]
try:
@@ -84,6 +90,11 @@ crud_router = TenantCRUDRoutes(
enable_filters=True, # Enable status, client_id, year filters
default_page_size=50,
max_page_size=1000,
list_permissions=["pedimentos_mgmt.view"],
get_permissions=["pedimentos_mgmt.view"],
create_permissions=["pedimentos_mgmt.create"],
update_permissions=["pedimentos_mgmt.edit"],
delete_permissions=["pedimentos_mgmt.delete"],
).router
# Include the CRUD routes into our main router

View File

@@ -1,5 +1,5 @@
import logging
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from typing import List, Union
@@ -38,6 +38,7 @@ router = APIRouter(
)
def get_temporary_import_movements(
filters: ImportTemporaryFilter,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
):
@@ -60,6 +61,9 @@ def get_temporary_import_movements(
f"User {current_user.get('preferred_username', 'unknown')} "
f"requesting temporary import movements"
)
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["report.view"])
movements = movement_service.get_temporary_import_movements(
db=db,
filters=filters
@@ -100,6 +104,7 @@ def get_temporary_import_movements(
)
def get_temporary_import_movements_detailed(
filters: ImportTemporaryFilter,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
):
@@ -122,6 +127,9 @@ def get_temporary_import_movements_detailed(
f"User {current_user.get('preferred_username', 'unknown')} "
f"requesting DETAILED temporary import movements"
)
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["report.view"])
movements = movement_service.get_temporary_import_movements_detailed(
db=db,
filters=filters
@@ -168,6 +176,7 @@ def get_temporary_import_movements_detailed(
)
def get_definitive_import_movements(
filters: ImportDefinitiveFilter,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
):
@@ -190,6 +199,9 @@ def get_definitive_import_movements(
f"User {current_user.get('preferred_username', 'unknown')} "
f"requesting definitive import movements"
)
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["report.view"])
movements = movement_service.get_definitive_import_movements(
db=db,
filters=filters
@@ -231,6 +243,7 @@ def get_definitive_import_movements(
)
def get_definitive_import_movements_detailed(
filters: ImportDefinitiveFilter,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
):
@@ -253,6 +266,9 @@ def get_definitive_import_movements_detailed(
f"User {current_user.get('preferred_username', 'unknown')} "
f"requesting DETAILED definitive import movements"
)
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["report.view"])
movements = movement_service.get_definitive_import_movements_detailed(
db=db,
filters=filters
@@ -299,6 +315,7 @@ def get_definitive_import_movements_detailed(
)
def get_repair_import_movements(
filters: ImportRepairFilter,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
):
@@ -321,6 +338,9 @@ def get_repair_import_movements(
f"User {current_user.get('preferred_username', 'unknown')} "
f"requesting repair import movements"
)
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["report.view"])
movements = movement_service.get_repair_import_movements(
db=db,
filters=filters
@@ -374,6 +394,7 @@ def get_repair_import_movements(
)
async def get_import_repair_movements_detailed(
filters: ImportRepairFilter,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
):
@@ -383,6 +404,9 @@ async def get_import_repair_movements_detailed(
"""
try:
logger.info(f"User {current_user.get('sub')} requesting detailed repair movements")
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["report.view"])
movements = movement_service.get_repair_import_movements_detailed(
db=db,
filters=filters
@@ -438,6 +462,7 @@ async def get_import_repair_movements_detailed(
)
async def get_export_movements(
filters: ExportFilter,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
):
@@ -447,6 +472,9 @@ async def get_export_movements(
"""
try:
logger.info(f"User {current_user.get('sub')} requesting export movements")
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["report.view"])
movements = movement_service.get_export_movements(
db=db,
filters=filters
@@ -509,6 +537,7 @@ async def get_export_movements(
)
async def get_export_movements_detailed(
filters: ExportFilter,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
):
@@ -518,6 +547,9 @@ async def get_export_movements_detailed(
"""
try:
logger.info(f"User {current_user.get('sub')} requesting detailed export movements")
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["report.view"])
movements = movement_service.get_export_movements_detailed(
db=db,
filters=filters
@@ -535,6 +567,7 @@ async def get_export_movements_detailed(
@router.post("/export-repair", response_model=List[MovementItem])
def get_export_repair_movements(
filters: ExportRepairFilter,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
):
@@ -555,6 +588,9 @@ def get_export_repair_movements(
"""
try:
logger.info(f"User {current_user.get('sub')} requesting export repair movements")
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["report.view"])
movements = movement_service.get_export_repair_movements(
db=db,
filters=filters
@@ -572,6 +608,7 @@ def get_export_repair_movements(
@router.post("/export-repair-detailed", response_model=List[MovementItemDetailed])
def get_export_repair_movements_detailed(
filters: ExportRepairFilter,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
):
@@ -581,6 +618,9 @@ def get_export_repair_movements_detailed(
"""
try:
logger.info(f"User {current_user.get('sub')} requesting detailed export repair movements")
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["report.view"])
movements = movement_service.get_export_repair_movements_detailed(
db=db,
filters=filters
@@ -611,6 +651,7 @@ def get_export_repair_movements_detailed(
)
async def get_all_movements(
filters: AllMovementsFilter,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
):
@@ -633,6 +674,9 @@ async def get_all_movements(
f"User {current_user.get('preferred_username', 'unknown')} "
f"requesting all invoice movements (send_email={filters.send_email})"
)
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["report.view"])
movements = movement_service.get_all_movements(
db=db,
filters=filters
@@ -701,6 +745,7 @@ async def get_all_movements(
)
def generate_invoice_report_async(
filters: AllMovementsFilter,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
):
@@ -709,9 +754,13 @@ def generate_invoice_report_async(
Returns task_id to poll status.
"""
from .tasks import generate_invoice_movements_async
from core.security import validate_access_to_resource
logger.info(f"User {current_user.get('preferred_username', 'unknown')} triggering async report generation")
# validate_access_to_resource returns the integer tenant_id from DB
tenant_id = validate_access_to_resource(db, company_id, current_user, ["report.process"])
# Serialize filters to dict for Celery
filter_data = filters.model_dump()
user_email = current_user.get('email')
@@ -729,7 +778,7 @@ def generate_invoice_report_async(
db=db,
task=generate_invoice_movements_async,
tenant_id=int(tenant_id),
company_id=filters.company_id,
company_id=company_id,
requested_by_user=current_user.get("preferred_username") or current_user.get("email") or current_user.get("sub"),
task_name="generate_invoice_movements_async",
task_group="reports",

View File

@@ -43,7 +43,7 @@ def generate_saldos_report_async(
)
# validate_access_to_resource returns the integer tenant_id from DB
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["report.process"])
# Inject scoping fields (not from the UI body)
filters.company_id = company_id

View File

@@ -32,7 +32,7 @@ async def list_drivers(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["drivers.view"])
drivers = DriverService.list_drivers(db, str(company_id), tenant_id)
total = len(drivers)
@@ -56,7 +56,7 @@ async def read_driver(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["drivers.view"])
driver = DriverService.get_driver_by_key_and_line(
db, transporter_key, line, str(company_id), tenant_id
)
@@ -73,7 +73,7 @@ async def create_driver(
):
# Validar acceso a la empresa del cuerpo
tenant_id = validate_access_to_resource(
db, driver_data.company_id, current_user
db, driver_data.company_id, current_user, required_permissions=["drivers.create"]
)
tk = (driver_data.transporter_key or "").strip()
# Buscar transportista: primero exacto, luego ignorando mayúsculas
@@ -124,7 +124,7 @@ async def update_driver(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["drivers.edit"])
try:
driver = DriverService.update_driver(
db,
@@ -152,7 +152,7 @@ async def delete_driver(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["drivers.delete"])
driver = DriverService.delete_driver(
db, transporter_key, line, str(company_id), tenant_id
)

View File

@@ -27,5 +27,10 @@ crud_router = TenantCRUDRoutes(
enable_filters=True,
default_page_size=50,
max_page_size=100,
list_permissions=["trailers.view"],
get_permissions=["trailers.view"],
create_permissions=["trailers.create"],
update_permissions=["trailers.edit"],
delete_permissions=["trailers.delete"],
).router
router.include_router(crud_router)

View File

@@ -1,18 +1,12 @@
from fastapi import APIRouter
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from .dto import TransporterCreateDTO, TransporterResponseDTO, TransporterUpdateDTO
from .services import TransporterService
from api.v1.modules.a76.layouts_csv.transportistas.routes import router as imports_router
# Main router: transporters CRUD + CSV imports
router = APIRouter()
# CSV import (upload → scan → status → commit)
router.include_router(imports_router, prefix="/transporters/imports", tags=["a76 / transporters / csv_import"])
# CRUD routes
crud_router = TenantCRUDRoutes(
service=TransporterService,
create_schema=TransporterCreateDTO,
@@ -21,11 +15,17 @@ crud_router = TenantCRUDRoutes(
prefix="/transporters",
tags=[],
resource_name="Transporter",
id_name="transporter_key", # Using transporter_key instead of numeric ID
id_type=str, # Specify that the ID is a string
enable_list=True, # Enable GET /transporters with pagination
enable_filters=True, # Enable filtering by name and rfc
id_name="transporter_key",
id_type=str,
enable_list=True,
enable_filters=True,
default_page_size=50,
max_page_size=100,
list_permissions=["transporters.view"],
get_permissions=["transporters.view"],
create_permissions=["transporters.create"],
update_permissions=["transporters.edit"],
delete_permissions=["transporters.delete"],
).router
router.include_router(crud_router)
router.include_router(crud_router)

View File

@@ -27,5 +27,10 @@ crud_router = TenantCRUDRoutes(
enable_filters=True,
default_page_size=50,
max_page_size=100,
list_permissions=["vehicles.view"],
get_permissions=["vehicles.view"],
create_permissions=["vehicles.create"],
update_permissions=["vehicles.edit"],
delete_permissions=["vehicles.delete"],
).router
router.include_router(crud_router)

View File

@@ -61,6 +61,7 @@ class UserInfoResponseDTO(BaseModel):
tenant_id: Optional[int] = None
tenant_slug: Optional[str] = None
roles: list[str] = []
permissions: list[str] = []
class Config:
json_schema_extra = {
@@ -71,6 +72,7 @@ class UserInfoResponseDTO(BaseModel):
"preferred_username": "jperez",
"tenant_id": 1,
"roles": ["user", "admin"],
"permissions": ["cat_ports.view", "cat_ports.create"]
}
}

View File

@@ -253,6 +253,40 @@ class AuthService:
if isinstance(tenant_slug, list):
tenant_slug = tenant_slug[0] if tenant_slug else None
# Obtener permisos del usuario en todas las compañías permitidas
permissions = set()
user_sub = user_info.get("sub")
if user_sub:
from api.v1.modules.core.permissions.service import PermissionService
from api.v1.modules.core.permissions.models import UserCompanyRole
perm_service = PermissionService(self.db)
# Obtener todas las compañías a las que el usuario tiene acceso
user_roles = self.db.query(UserCompanyRole.company_id).filter(
UserCompanyRole.user_id == user_sub,
UserCompanyRole.is_active == True
).distinct().all()
# Unir los permisos de todas las compañías para alimentar la UI
for (cid,) in user_roles:
permissions.update(perm_service.get_user_permissions(user_sub, cid))
# 🛡️ MEJORA DEV: Si es admin de Keycloak O estamos en desarrollo y no tiene permisos locales aún.
# Esto evita el "lockout" cuando se reinicia el proyecto para todos los usuarios.
from core.config import settings
if "admin" in roles or (settings.ENVIRONMENT == "development" and not permissions):
try:
from api.v1.modules.core.permissions.registry import registry as perm_registry
# Asegurar que los permisos core estén registrados
from api.v1.modules.core.permissions import seed_v2
all_registered = [p.code for p in perm_registry.get_all()]
permissions.update(all_registered)
logger.info(f"God Mode (Dev): Otorgando {len(all_registered)} permisos al usuario {user_sub}")
except Exception as e:
logger.error(f"Error in God Mode bootstrap: {e}")
permissions = list(permissions)
return UserInfoResponseDTO(
sub=user_info.get("sub"),
email=user_info.get("email"),
@@ -261,6 +295,7 @@ class AuthService:
tenant_id=int(tenant_id) if tenant_id else None,
tenant_slug=tenant_slug,
roles=roles,
permissions=permissions,
)
except KeycloakError as e:

View File

@@ -18,6 +18,10 @@ from .dependencies import (
get_current_user_permissions,
)
from .routes import router
from .seed_v2 import register_core_permissions
# Registrar permisos al cargar el módulo
register_core_permissions()
__all__ = [
# Models

View File

@@ -0,0 +1,65 @@
"""
Script CLI para LIMPIEZA TOTAL del sistema de permisos.
Borra todos los roles, asignaciones y el catálogo de permisos.
Úselo con precaución.
Uso:
docker exec -it <container> python3 -m api.v1.modules.core.permissions.cleanup_cli
"""
import sys
import os
import logging
from sqlalchemy import text
# Configurar logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger(__name__)
# Asegurar que el backend esté en el path
sys.path.append(os.path.abspath("."))
sys.path.append(os.path.abspath("backend"))
from core.database import CoreSessionLocal
def run_cleanup():
"""Ejecuta el borrado de tablas en orden de dependencias."""
logger.warning("INICIANDO LIMPIEZA TOTAL DE PERMISOS Y ROLES...")
db = CoreSessionLocal()
try:
# 1. Borrar asignaciones directas de permisos a usuarios
logger.info("Borrando asignaciones directas de usuario...")
db.execute(text("DELETE FROM core.user_company_permissions"))
# 2. Borrar relación entre roles y permisos
logger.info("Borrando mapeo de roles y permisos...")
db.execute(text("DELETE FROM core.role_permissions"))
# 3. Borrar asignación de roles a usuarios
logger.info("Borrando asignación de roles a usuarios...")
db.execute(text("DELETE FROM core.user_company_roles"))
# 4. Borrar los roles mismos
logger.info("Borrando el catálogo de roles...")
db.execute(text("DELETE FROM core.company_roles"))
# 5. Borrar el catálogo base de permisos
logger.info("Borrando el catálogo base de permisos...")
db.execute(text("DELETE FROM core.permissions"))
db.commit()
logger.info("=" * 40)
logger.info("LIMPIEZA COMPLETADA CON ÉXITO")
logger.info("El sistema de permisos está ahora en blanco.")
logger.info("=" * 40)
except Exception as e:
db.rollback()
logger.error(f"Error crítico durante la limpieza: {e}")
sys.exit(1)
finally:
db.close()
if __name__ == "__main__":
run_cleanup()

View File

@@ -0,0 +1,101 @@
"""
Registro centralizado para la modulación de permisos.
Permite que cada módulo registre sus propios permisos de forma dinámica.
"""
import logging
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, field
logger = logging.getLogger(__name__)
@dataclass
class PermissionDefinition:
"""Representa la definición de un permiso en un módulo."""
code: str
description: Optional[str] = None
module: Optional[str] = None
action: Optional[str] = None
is_active: bool = True
def __post_init__(self):
"""Lógica de autocompletado para evitar redundancia."""
# Si el código tiene el formato "modulo.sub.accion" o "modulo.accion"
parts = self.code.split(".")
# Extraer módulo si no se especificó
if not self.module and len(parts) > 1:
self.module = parts[0]
elif not self.module:
self.module = "system" # Default fallback
# Extraer acción si no se especificó (es la última parte del código)
if not self.action and len(parts) > 1:
self.action = parts[-1]
elif not self.action:
self.action = "view" # Default fallback
class PermissionRegistry:
"""
Registro Singleton para permisos de la aplicación.
Cada módulo de la API debe importar este registro y dar de alta sus permisos.
"""
_instance = None
_permissions: Dict[str, PermissionDefinition] = {}
def __new__(cls):
if cls._instance is None:
cls._instance = super(PermissionRegistry, cls).__new__(cls)
cls._permissions = {}
return cls._instance
@classmethod
def register(cls,
code: str,
description: Optional[str] = None,
module: Optional[str] = None,
action: Optional[str] = None) -> None:
"""
Registra un nuevo permiso en el sistema.
"""
if code in cls._permissions:
logger.debug(f"Permiso {code} ya está registrado, actualizando metadatos.")
cls._permissions[code] = PermissionDefinition(
code=code,
description=description,
module=module,
action=action
)
@classmethod
def register_many(cls, permissions_list: List[tuple]) -> None:
"""
Registra múltiples permisos desde una lista de tuplas.
Útil para migrar seeds estáticos.
"""
for item in permissions_list:
if len(item) == 2: # (code, description)
cls.register(code=item[0], description=item[1])
elif len(item) >= 3: # (code, description, module, ...)
cls.register(
code=item[0],
description=item[1],
module=item[2],
action=item[3] if len(item) > 3 else None
)
@classmethod
def get_all(cls) -> List[PermissionDefinition]:
"""Retorna todos los permisos registrados."""
return list(cls._permissions.values())
@classmethod
def get_by_module(cls, module_name: str) -> List[PermissionDefinition]:
"""Retorna los permisos de un módulo específico."""
return [p for p in cls._permissions.values() if p.module == module_name]
# Instancia global para facilitar el acceso
registry = PermissionRegistry()

View File

@@ -42,9 +42,7 @@ from .schemas import (
router = APIRouter(prefix="/permissions", tags=["permissions"])
# ============================================================================
# RUTAS DE CONSULTA DE PERMISOS
# ============================================================================
@router.get("/me", response_model=UserPermissionsResponse)
@@ -55,17 +53,45 @@ async def get_my_permissions(
permission_service: PermissionService = Depends(get_permission_service),
):
"""
Obtiene los permisos y roles del usuario actual en el company actual.
No requiere permisos especiales ya que es información propia.
Obtiene los permisos y roles del usuario actual en la compañía actual.
Si la compañía no tiene roles definidos, realiza un bootstrap automático.
También realiza bootstrap si es un usuario con rol 'admin' en Keycloak pero sin roles locales.
"""
# 1. Validar acceso básico a la compañía
# Nota: pass None en required_permissions permite el paso al bootstrap
tenant_id = validate_access_to_resource(db, company_id, current_user)
user_id = current_user.get("sub") or current_user.get("id")
# Obtener permisos
# 2. Determinar si es un admin de Keycloak para forzar bootstrap si es necesario
realm_roles = current_user.get("realm_access", {}).get("roles", [])
client_roles = []
for client in current_user.get("resource_access", {}).values():
client_roles.extend(client.get("roles", []))
is_keycloak_admin = "admin" in realm_roles or "admin" in client_roles
# 3. Bootstrap: si la compañía no tiene roles, o si el usuario es admin, o si estamos en desarrollo y el usuario no tiene roles
from .models import CompanyRole, UserCompanyRole
from core.config import settings
has_roles = db.query(CompanyRole).filter(CompanyRole.company_id == company_id).first() is not None
user_has_roles = db.query(UserCompanyRole).filter(
UserCompanyRole.user_id == user_id,
UserCompanyRole.company_id == company_id
).first() is not None
auto_bootstrap = (not has_roles) or \
(is_keycloak_admin and not user_has_roles) or \
(settings.ENVIRONMENT == "development" and not user_has_roles)
if auto_bootstrap:
# Nota: bootstrap_super_admin ya hace commit e intenta no duplicar si el rol ya existe
permission_service.bootstrap_super_admin(user_id, company_id)
# 4. Obtener permisos finales
permissions = permission_service.get_user_permissions(user_id, company_id)
# Obtener roles
# 5. Obtener roles
roles = permission_service.get_user_roles(user_id, company_id)
role_names = [role.name for role in roles]
@@ -81,9 +107,9 @@ async def get_my_permissions(
async def list_available_permissions(
db: Session = Depends(get_core_db),
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(100, ge=1, le=500, description="Tamaño de página"),
page_size: int = Query(100, ge=1, le=1000, description="Tamaño de página"),
module: Optional[str] = Query(None, description="Filtrar por módulo"),
_: None = Depends(RequirePermission("permissions.view")),
_: None = Depends(RequirePermission("roles.view")),
):
"""
Lista todos los permisos disponibles en el sistema.
@@ -108,13 +134,13 @@ async def list_company_roles(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(100, ge=1, le=500, description="Tamaño de página"),
page_size: int = Query(100, ge=1, le=1000, description="Tamaño de página"),
):
"""
Lista todos los roles del companye actual.
TODO: Agregar verificación de permisos
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
query = db.query(CompanyRole).filter(
CompanyRole.company_id == company_id
@@ -139,7 +165,7 @@ async def get_user_permissions(
Obtiene los permisos y roles de un usuario específico.
Requiere permiso: user.view
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
permissions = permission_service.get_user_permissions(user_id, company_id)
roles = permission_service.get_user_roles(user_id, company_id)
@@ -153,16 +179,14 @@ async def get_user_permissions(
)
# ============================================================================
# RUTAS CRUD DE PERMISOS
# ============================================================================
@router.get("", response_model=PermissionListResponse)
async def list_permissions(
db: Session = Depends(get_core_db),
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(100, ge=1, le=500, description="Tamaño de página"),
page_size: int = Query(100, ge=1, le=1000, description="Tamaño de página"),
module: Optional[str] = Query(None, description="Filtrar por módulo"),
action: Optional[str] = Query(None, description="Filtrar por acción"),
search: Optional[str] = Query(None, description="Buscar por código o descripción"),
@@ -229,9 +253,7 @@ async def get_actions(
return [a[0] for a in actions]
# ============================================================================
# RUTAS DE GESTIÓN DE ASIGNACIONES DE ROLES A USUARIOS
# ============================================================================
@router.get("/user-roles", response_model=UserRoleListResponse)
@@ -241,7 +263,7 @@ async def list_user_roles(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(100, ge=1, le=500, description="Tamaño de página"),
page_size: int = Query(100, ge=1, le=1000, description="Tamaño de página"),
):
"""
Lista todas las asignaciones de roles a usuarios en la compañía.
@@ -249,7 +271,7 @@ async def list_user_roles(
from .models import UserCompanyRole
from sqlalchemy.orm import joinedload
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
query = db.query(UserCompanyRole).options(
joinedload(UserCompanyRole.company_role)
@@ -286,7 +308,7 @@ async def assign_user_role(
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import joinedload
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
assigner_id = current_user.get("sub") or current_user.get("id")
# Verificar que el rol existe
@@ -342,7 +364,7 @@ async def remove_user_role(
"""
from .models import UserCompanyRole
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
user_role = db.query(UserCompanyRole).filter(
UserCompanyRole.id == user_role_id,
@@ -380,6 +402,26 @@ async def get_permission(
return permission
@router.post("/sync", response_model=Dict[str, Any])
async def sync_permissions_endpoint(
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
permission_service: PermissionService = Depends(get_permission_service),
_: None = Depends(RequirePermission("roles.edit")),
):
"""
Sincroniza dinámicamente los permisos registrados en los módulos con la base de datos.
Requiere permiso: permissions.edit
"""
result = permission_service.sync_permissions()
return {
"success": True,
"message": "Permissions synchronized successfully",
"data": result
}
from .schemas import CreatePermissionRequest
@@ -470,9 +512,7 @@ async def delete_permission(
db.commit()
# ============================================================================
# RUTAS DE GESTIÓN DE ROLES
# ============================================================================
@router.post(
@@ -488,7 +528,7 @@ async def create_role(
Crea un nuevo rol personalizado para el companye.
TODO: Agregar verificación de permisos
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.create"])
# Verificar que el código no esté en uso
existing = (
@@ -540,7 +580,7 @@ async def update_role(
Actualiza un rol existente.
TODO: Agregar verificación de permisos
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.edit"])
role = (
db.query(CompanyRole)
@@ -580,7 +620,7 @@ async def delete_role(
"""
from .models import RolePermission, UserCompanyRole
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.delete"])
role = (
db.query(CompanyRole)
@@ -612,9 +652,7 @@ async def delete_role(
db.commit()
# ============================================================================
# RUTAS DE GESTIÓN DE PERMISOS POR ROL
# ============================================================================
@router.get("/roles/{role_id}/permissions")
@@ -629,7 +667,7 @@ async def get_role_permissions(
"""
from .models import RolePermission
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
# Verificar que el rol existe y pertenece al company
role = (
@@ -687,7 +725,7 @@ async def assign_permission_to_role(
"""
from .models import RolePermission
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
# Verificar que el rol existe
role = (
@@ -753,7 +791,7 @@ async def assign_multiple_permissions_to_role(
"""
from .models import RolePermission
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
# Verificar que el rol existe
role = (
@@ -822,7 +860,7 @@ async def remove_permission_from_role(
"""
from .models import RolePermission
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
# Buscar la asignación
role_permission = (
@@ -846,9 +884,7 @@ async def remove_permission_from_role(
return {"success": True, "message": "Permission removed from role"}
# ============================================================================
# RUTAS DE ASIGNACIÓN DE ROLES Y PERMISOS
# ============================================================================
@router.post(
@@ -865,7 +901,7 @@ async def assign_role(
Asigna un rol a un usuario en el companye actual.
TODO: Agregar verificación de permisos
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
assigner_id = current_user.get("sub") or current_user.get("id")
@@ -902,7 +938,7 @@ async def grant_permission(
Concede un permiso directo a un usuario en el companye actual.
Requiere permiso: permissions.grant
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
assigner_id = current_user.get("sub") or current_user.get("id")
@@ -930,9 +966,7 @@ async def grant_permission(
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
# ============================================================================
# EJEMPLOS DE RUTAS PROTEGIDAS CON PERMISOS
# ============================================================================
@router.get("/examples/invoices")
@@ -1016,9 +1050,7 @@ async def example_dashboard(
return {"permissions": list(user_permissions), "widgets": widgets}
# ============================================================================
# RUTAS DE GESTIÓN DE PERMISOS INDIVIDUALES DE USUARIO
# ============================================================================
@router.get("/users/{user_id}/permissions")
@@ -1035,7 +1067,7 @@ async def get_user_individual_permissions(
from .models import UserCompanyPermission
from sqlalchemy.orm import joinedload
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
user_permissions = (
db.query(UserCompanyPermission)
@@ -1072,7 +1104,7 @@ async def get_user_effective_permissions(
from .models import UserCompanyPermission, UserCompanyRole, RolePermission
from sqlalchemy.orm import joinedload
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
# 1. Obtener permisos de roles
role_permissions_query = (
@@ -1153,7 +1185,7 @@ async def assign_user_permission(
"""
from .models import UserCompanyPermission
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
# Verificar que el permiso existe
permission = db.query(Permission).filter(Permission.id == request.permission_id).first()
@@ -1216,7 +1248,7 @@ async def remove_user_permission(
"""
from .models import UserCompanyPermission
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["roles.view"])
user_permission = (
db.query(UserCompanyPermission)

View File

@@ -17,47 +17,58 @@ seed_invoices = [
("invoice.imp.cm.delete", "Eliminar facturas", "invoice", "delete"),
("invoice.imp.cm.process", "Procesar facturas", "invoice", "process"),
("invoice.imp.cr.view", "Ver facturas temporales de importación", "invoice", "view"),
("invoice.imp.cr.create", "Crear facturas temporales de importación", "invoice", "create"),
("invoice.imp.cr.view", "Ver facturas de importación con corrección", "invoice", "view"),
("invoice.imp.cr.create", "Crear facturas de importación con corrección", "invoice", "create"),
("invoice.imp.cr.edit", "Editar facturas", "invoice", "edit"),
("invoice.imp.cr.delete", "Eliminar facturas", "invoice", "delete"),
("invoice.imp.cr.process", "Procesar facturas", "invoice", "process"),
("invoice.exp.view", "Ver facturas temporales de importación", "invoice", "view"),
("invoice.exp.create", "Crear facturas temporales de importación", "invoice", "create"),
("invoice.exp.edit", "Editar facturas", "invoice", "edit"),
("invoice.exp.delete", "Eliminar facturas", "invoice", "delete"),
("invoice.exp.process", "Procesar facturas", "invoice", "process"),
("invoice.exp.rep.view", "Ver facturas temporales de importación", "invoice", "view"),
("invoice.exp.rep.create", "Crear facturas temporales de importación", "invoice", "create"),
("invoice.exp.rep.edit", "Editar facturas", "invoice", "edit"),
("invoice.exp.rep.delete", "Eliminar facturas", "invoice", "delete"),
("invoice.exp.rep.process", "Procesar facturas", "invoice", "process"),
("invoice.imp.rep.view", "Ver facturas de importación reparación (Activo Fijo)", "invoice", "view"),
("invoice.imp.rep.create", "Crear facturas de importación reparación (Activo Fijo)", "invoice", "create"),
("invoice.imp.rep.edit", "Editar facturas de importación reparación", "invoice", "edit"),
("invoice.imp.rep.delete", "Eliminar facturas de importación reparación", "invoice", "delete"),
("invoice.imp.rep.process", "Procesar facturas de importación reparación", "invoice", "process"),
("invoice.exp.view", "Ver facturas de exportación", "invoice", "view"),
("invoice.exp.create", "Crear facturas de exportación", "invoice", "create"),
("invoice.exp.edit", "Editar facturas de exportación", "invoice", "edit"),
("invoice.exp.delete", "Eliminar facturas de exportación", "invoice", "delete"),
("invoice.exp.process", "Procesar facturas de exportación", "invoice", "process"),
("invoice.exp.rep.view", "Ver facturas de exportación reparación", "invoice", "view"),
("invoice.exp.rep.create", "Crear facturas de exportación reparación", "invoice", "create"),
("invoice.exp.rep.edit", "Editar facturas de exportación reparación", "invoice", "edit"),
("invoice.exp.rep.delete", "Eliminar facturas de exportación reparación", "invoice", "delete"),
("invoice.exp.rep.process", "Procesar facturas de exportación reparación", "invoice", "process"),
("invoice.exp.donac.view", "Ver facturas de exportación donación", "invoice", "view"),
("invoice.exp.donac.create", "Crear facturas de exportación donación", "invoice", "create"),
("invoice.exp.donac.edit", "Editar facturas de exportación donación", "invoice", "edit"),
("invoice.exp.donac.delete", "Eliminar facturas de exportación donación", "invoice", "delete"),
("invoice.exp.donac.process", "Procesar facturas de exportación donación", "invoice", "process"),
]
seed_user = [
("user.view", "Ver facturas temporales de importación", "invoice", "view"),
("user.create", "Crear facturas temporales de importación", "invoice", "create"),
("user.edit", "Editar facturas", "invoice", "edit"),
("user.delete", "Eliminar facturas", "invoice", "delete"),
("user.process", "Procesar facturas", "invoice", "process"),
("user.view", "Ver Usuarios", "invoice", "view"),
("user.create", "Crear Usuarios", "invoice", "create"),
("user.edit", "Editar Usuarios", "invoice", "edit"),
("user.delete", "Eliminar Usuarios", "invoice", "delete"),
("user.process", "Procesar Usuarios", "invoice", "process"),
]
seed_report = [
("report.view", "Ver facturas temporales de importación", "invoice", "view"),
("report.create", "Crear facturas temporales de importación", "invoice", "create"),
("report.edit", "Editar facturas", "invoice", "edit"),
("report.delete", "Eliminar facturas", "invoice", "delete"),
("report.process", "Procesar facturas", "invoice", "process"),
("report.view", "Ver Reportes", "invoice", "view"),
("report.create", "Crear Reportes", "invoice", "create"),
("report.edit", "Editar Reportes", "invoice", "edit"),
("report.delete", "Eliminar Reportes", "invoice", "delete"),
("report.process", "Procesar Reportes", "invoice", "process"),
]
seed_roles = [
("roles.view", "Ver facturas temporales de importación", "invoice", "view"),
("roles.create", "Crear facturas temporales de importación", "invoice", "create"),
("roles.edit", "Editar facturas", "invoice", "edit"),
("roles.delete", "Eliminar facturas", "invoice", "delete"),
("roles.process", "Procesar facturas", "invoice", "process"),
("roles.view", "Ver Roles", "invoice", "view"),
("roles.create", "Crear Roles", "invoice", "create"),
("roles.edit", "Editar Roles", "invoice", "edit"),
("roles.delete", "Eliminar Roles", "invoice", "delete"),
("roles.process", "Procesar Roles", "invoice", "process"),
]

View File

@@ -0,0 +1,232 @@
"""
Semilla V2 para el sistema de permisos.
Corrige las descripciones erróneas y organiza los permisos por módulos reales.
Utiliza el PermissionRegistry para la modulación por API.
"""
from .registry import registry
from .seed import seed_invoices, seed_user, seed_report, seed_roles
# ============================================================================
# HELPER: LIMPIEZA DE MÓDULOS DESDE SEED.PY
# ============================================================================
def clean_seed_permissions(perms):
"""
Toma los permisos de seed.py (que vienen todos como module='invoice')
y los re-asigna al módulo correcto según su prefijo para la UI.
"""
cleaned = []
for code, desc, _, action in perms:
module = "invoice_imp" # Default
if code.startswith("user."): module = "users"
elif code.startswith("report."): module = "reports"
elif code.startswith("roles."): module = "roles"
elif code.startswith("invoice.exp"): module = "invoice_exp"
cleaned.append((code, desc, module, action))
return cleaned
# ============================================================================
# PERMISOS DE USUARIOS (CORREGIDOS)
# ============================================================================
# ============================================================================
# AUDITORÍA Y BITÁCORA
# ============================================================================
permissions_audit = [
("audit_logs.view", "Ver bitácora de movimientos", "audit_logs", "view"),
]
# ============================================================================
# DATOS DE REFERENCIA (Catálogos Fijos - Solo Ver)
# ============================================================================
permissions_reference = [
("ref_pedimento_regimens.view", "Ver Regímenes de Pedimento", "reference_data", "view"),
("ref_containers.view", "Ver Contenedores", "reference_data", "view"),
("ref_countries.view", "Ver Países", "reference_data", "view"),
("ref_currency_types.view", "Ver Tipos de Moneda", "reference_data", "view"),
("ref_customs_sections.view", "Ver Secciones Aduaneras", "reference_data", "view"),
("ref_customs_warehouses.view", "Ver Recintos Fiscalizados", "reference_data", "view"),
("ref_incoterms.view", "Ver Incoterms", "reference_data", "view"),
("ref_invoice_types.view", "Ver Tipos de Factura", "reference_data", "view"),
("ref_material_types.view", "Ver Tipos de Material", "reference_data", "view"),
("ref_payment_methods.view", "Ver Formas de Pago", "reference_data", "view"),
("ref_pedimento_codes.view", "Ver Claves de Pedimento", "reference_data", "view"),
("ref_sectors.view", "Ver Sectores", "reference_data", "view"),
("ref_states.view", "Ver Estados", "reference_data", "view"),
("ref_transport_modes.view", "Ver Modos de Transporte", "reference_data", "view"),
("ref_transport_types.view", "Ver Tipos de Transporte", "reference_data", "view"),
("ref_valuation_methods.view", "Ver Métodos de Valoración", "reference_data", "view"),
]
# ============================================================================
# CATÁLOGOS GENERALES (CRUD)
# ============================================================================
def gen_crud(resource, desc, module):
return [
(f"{resource}.view", f"Ver {desc}", module, "view"),
(f"{resource}.create", f"Crear {desc}", module, "create"),
(f"{resource}.edit", f"Editar {desc}", module, "edit"),
(f"{resource}.delete", f"Eliminar {desc}", module, "delete"),
]
permissions_general = (
gen_crud("cat_company", "Información de la Empresa", "general_catalogs") +
gen_crud("cat_packages", "Bultos", "general_catalogs") +
gen_crud("cat_concepts", "Conceptos", "general_catalogs") +
gen_crud("cat_broker_concepts", "Conceptos de Agente Aduanal", "general_catalogs") +
gen_crud("cat_classification", "Clasificación de Conceptos", "general_catalogs") +
gen_crud("cat_identifiers", "Identificadores", "general_catalogs") +
gen_crud("cat_incoterms", "Incoterms (Catálogo)", "general_catalogs") +
gen_crud("cat_inpc", "INPC", "general_catalogs") +
gen_crud("cat_legends", "Leyendas Fijas", "general_catalogs") +
gen_crud("cat_seals", "Sellos", "general_catalogs") +
gen_crud("cat_valuation", "Métodos de Valoración", "general_catalogs") +
gen_crud("cat_countries", "Países", "general_catalogs") +
gen_crud("cat_ports", "Puertos", "general_catalogs") +
gen_crud("cat_um_general", "UdM Generales", "general_catalogs") +
gen_crud("cat_um_customs", "UdM Aduana MEX", "general_catalogs") +
gen_crud("cat_um_american", "UdM Aduana AME", "general_catalogs") +
gen_crud("cat_um_ace", "UdM ACE", "general_catalogs") +
gen_crud("cat_um_oma", "UdM OMA", "general_catalogs") +
gen_crud("cat_unit_conversions", "Conversiones", "general_catalogs") +
gen_crud("cat_equivalencies", "Equivalencias", "general_catalogs") +
gen_crud("cat_exchange_rates", "Tipos de Cambio", "general_catalogs") +
gen_crud("cat_currency", "Tipos de Moneda", "general_catalogs") +
gen_crud("cat_multi_currency_types", "Tipos de Moneda (Múltiples)", "general_catalogs") +
gen_crud("cat_inv_types", "Tipos de Factura", "general_catalogs") +
gen_crud("cat_signatures", "Firmas Electrónicas", "general_catalogs") +
gen_crud("cat_errors", "Catálogo de Errores", "general_catalogs") +
gen_crud("cat_doda", "DODA", "general_catalogs") +
gen_crud("cat_prevalidators", "Prevalidadores", "general_catalogs") +
gen_crud("cat_notices", "Avisos Electrónicos", "general_catalogs") +
gen_crud("cat_crossing", "Avisos de Cruce", "general_catalogs") +
gen_crud("cat_warehouses", "Recintos", "general_catalogs") +
gen_crud("cat_sectors", "Sectores", "general_catalogs")
)
# ============================================================================
# FRACCIONES
# ============================================================================
permissions_fractions = [
# Solo ver para SITAR
("frac_sitar.view", "Ver Fracciones SITAR", "fractions", "view"),
("frac_sitar_7.view", "Ver Fracciones SITAR (Séptima Enmienda)", "fractions", "view"),
("frac_sitar_us.view", "Ver Fracciones SITAR (USA)", "fractions", "view"),
] + gen_crud("frac_american", "Fracciones Americanas", "fractions") + \
gen_crud("frac_canadian", "Fracciones Canadienses", "fractions") + \
gen_crud("frac_historical", "Fracciones Históricas", "fractions") + \
[("frac_sectors.view", "Ver Sectores", "fractions", "view")]
# ============================================================================
# TRANSPORTES (CRUD)
# ============================================================================
permissions_transports = (
gen_crud("transporters", "Transportistas", "transports") +
gen_crud("drivers", "Conductores", "transports") +
gen_crud("trailers", "Trailers", "transports") +
gen_crud("vehicles", "Vehículos", "transports")
)
# ============================================================================
# MERCANCÍAS (CRUD)
# ============================================================================
permissions_goods = (
gen_crud("goods_classes", "Clases de Activo Fijo", "goods") +
gen_crud("goods_parts", "Partes / Productos", "goods") +
gen_crud("goods_fda", "Códigos FDA", "goods")
)
# ============================================================================
# PEDIMENTOS
# ============================================================================
permissions_pedimentos = (
gen_crud("pedimentos_mgmt", "Gestión de Pedimentos", "pedimentos") +
[
("pedimentos_codes.view", "Ver Claves de Pedimento", "pedimentos", "view"),
("pedimentos_regimes.view", "Ver Regímenes Aduanales", "pedimentos", "view"),
("pedimentos_payments.view", "Ver Formas de Pago", "pedimentos", "view"),
("pedimentos_sections.view", "Ver Secciones Aduaneras", "pedimentos", "view"),
("pedimentos_anexo22.view", "Ver Anexo 22 / Apéndice 31", "pedimentos", "view"),
]
)
# ============================================================================
# EXPORTACIÓN
# ============================================================================
permissions_export = (
[("export_catalog.view", "Ver Catálogo Base Exportación", "export", "view")] +
[("export_repair.view", "Ver Reparaciones", "export", "view")] +
gen_crud("export_manifest", "Manifiestos de Exportación", "export") +
[("export_proforma.view", "Ver Proformas", "export", "view")] +
[("export_reports.view", "Ver Reportes Oficiales Exportación", "export", "view")] +
[("export_used.view", "Ver Materiales Utilizados", "export", "view")] +
[("export_destruction.view", "Ver Destrucciones", "export", "view")] +
[("export_special.view", "Ver Procesos Especiales", "export", "view")]
)
# ============================================================================
# FACTURACIÓN, USUARIOS, REPORTES Y ROLES (DESDE SEED.PY)
# ============================================================================
# Estas secciones se nutren directamente de seed.py
permissions_invoice_imp_seed = clean_seed_permissions(seed_invoices)
permissions_users_seed = clean_seed_permissions(seed_user)
permissions_reports_seed = clean_seed_permissions(seed_report)
permissions_roles_seed = clean_seed_permissions(seed_roles)
# ============================================================================
# CLIENTES Y PROVEEDORES (CRUD)
# ============================================================================
permissions_clients = gen_crud("partners_mgmt", "Clientes y Proveedores", "partners")
# ============================================================================
# AGENTES ADUANALES (CRUD)
# ============================================================================
permissions_brokers = gen_crud("customs_brokers", "Agentes Aduanales", "brokers")
# ======================================# USUARIOS DEL SISTEMA (YA NO SE USA AQUÍ, VIENE DE SEED.PY)
# ============================================================================
# permissions_users = ...
# ============================================================================
# CONFIGURACIÓN GENERAL Y AYUDA
# ============================================================================
permissions_settings = [
("settings_general.view", "Ver Configuración General", "settings", "view"),
("settings_general.edit", "Editar Configuración General", "settings", "edit"),
]
permissions_help = gen_crud("help_center", "Centro de Ayuda y Documentos", "help")
# ============================================================================
# CSV UPLOAD MASIVO
# ============================================================================
permissions_csv = [
("csv_upload.process", "Procesar Cargas Masivas CSV", "csv_upload", "process"),
]
def register_core_permissions():
"""Registra los permisos granulados de la aplicación según Sidebar."""
registry.register_many(permissions_audit)
registry.register_many(permissions_reference)
registry.register_many(permissions_general)
registry.register_many(permissions_fractions)
registry.register_many(permissions_transports)
registry.register_many(permissions_goods)
registry.register_many(permissions_pedimentos)
# Registro de permisos desde seed.py (limpios)
registry.register_many(permissions_invoice_imp_seed)
registry.register_many(permissions_users_seed)
registry.register_many(permissions_reports_seed)
registry.register_many(permissions_roles_seed)
registry.register_many(permissions_export)
registry.register_many(permissions_clients)
registry.register_many(permissions_brokers)
registry.register_many(permissions_settings)
registry.register_many(permissions_help)
registry.register_many(permissions_csv)
# Ejecutar registro al importar este módulo
register_core_permissions()

View File

@@ -296,3 +296,153 @@ class PermissionService:
self.db.refresh(user_permission)
return user_permission
def sync_permissions(self) -> dict:
"""
Sincroniza los permisos registrados en el registry con la base de datos.
Inserta nuevos permisos y actualiza los existentes.
"""
from .registry import registry
# IMPORTANTE: Importar seed_v2 para que se ejecute register_core_permissions()
from . import seed_v2
registered_permissions = registry.get_all()
synced_count = 0
updated_count = 0
for p_def in registered_permissions:
# Buscar permiso existente
db_permission = self.db.query(Permission).filter(Permission.code == p_def.code).first()
if db_permission:
# Actualizar si hay cambios
changed = False
if db_permission.description != p_def.description:
db_permission.description = p_def.description
changed = True
if db_permission.module != p_def.module:
db_permission.module = p_def.module
changed = True
if db_permission.action != p_def.action:
db_permission.action = p_def.action
changed = True
if db_permission.is_active != p_def.is_active:
db_permission.is_active = p_def.is_active
changed = True
if changed:
updated_count += 1
else:
# Crear nuevo
new_permission = Permission(
code=p_def.code,
description=p_def.description,
module=p_def.module,
action=p_def.action,
is_active=p_def.is_active
)
self.db.add(new_permission)
synced_count += 1
self.db.commit()
return {
"synced": synced_count,
"updated": updated_count,
"total_registered": len(registered_permissions)
}
def bootstrap_super_admin(self, user_id: str, company_id: int) -> bool:
"""
Crea un rol de Super Administrador con todos los permisos y se lo asigna al usuario.
Diseñado para el primer inicio de una compañía o para asegurar acceso a administradores.
"""
from .models import CompanyRole, RolePermission, UserCompanyRole, Permission
import logging
logger = logging.getLogger(__name__)
try:
# 0. Sincronizar permisos por si la tabla esta vacía
# Esto puebla la tabla 'permissions' desde el registry de código
logger.info("Bootstrap: Sincronizando catálogo de permisos desde el registry...")
from . import seed_v2
sync_res = self.sync_permissions()
logger.info(f"Bootstrap: Sincronización completa. {sync_res.get('synced', 0)} nuevos, {sync_res.get('total_registered', 0)} totales.")
# 1. Obtener el tenant_id
tenant_id = 1
try:
from api.v1.modules.a76.general_catalogs.company.models import Company
company = self.db.query(Company).filter(Company.id == company_id).first()
if company:
tenant_id = company.tenant_id
except:
pass
# 2. Buscar si ya existe el rol "super_admin"
admin_role = self.db.query(CompanyRole).filter(
CompanyRole.company_id == company_id,
CompanyRole.code == "super_admin"
).first()
if not admin_role:
logger.info(f"Bootstrap: Creando Super Administrador para usuario {user_id} (Company: {company_id})")
# Crear el rol Super Administrador si no existe
admin_role = CompanyRole(
company_id=company_id,
tenant_id=tenant_id,
name="Super Administrador",
code="super_admin",
description="Rol con acceso total al sistema (generado automáticamente)",
is_active=True
)
self.db.add(admin_role)
self.db.flush()
# 4. Asignar TODOS los permisos activos al rol
all_perms = self.db.query(Permission).filter(Permission.is_active == True).all()
if not all_perms:
logger.warning("Bootstrap: ¡ALERTA! No se encontraron permisos en la DB ni tras la sincronización.")
for perm in all_perms:
role_perm = RolePermission(
company_role_id=admin_role.id,
permission_id=perm.id,
tenant_id=tenant_id,
company_id=company_id
)
self.db.add(role_perm)
else:
logger.info(f"Bootstrap: El rol super_admin ya existe para la compañía {company_id}.")
# 5. Asegurar que el usuario tenga el rol asignado
user_has_role = self.db.query(UserCompanyRole).filter(
UserCompanyRole.user_id == user_id,
UserCompanyRole.company_id == company_id,
UserCompanyRole.company_role_id == admin_role.id
).first()
if not user_has_role:
logger.info(f"Bootstrap: Asignando rol Super Administrador al usuario {user_id}")
user_role = UserCompanyRole(
user_id=user_id,
company_id=company_id,
tenant_id=tenant_id,
company_role_id=admin_role.id,
is_active=True,
assigned_by="SYSTEM_BOOTSTRAP"
)
self.db.add(user_role)
self.db.commit()
return True
else:
logger.info(f"Bootstrap: El usuario {user_id} ya tiene el rol asignado.")
self.db.commit()
return False
except Exception as e:
self.db.rollback()
logger.error(f"Bootstrap: ERROR CRÍTICO - {str(e)}")
import traceback
logger.error(traceback.format_exc())
return False

View File

@@ -0,0 +1,50 @@
"""
Script CLI para sincronizar los permisos de la aplicación.
Útil para el bootstrap inicial cuando el endpoint /sync aún no es accesible
o cuando se desea forzar una actualización desde la consola/Docker.
Uso:
docker exec -it <container> python3 -m api.v1.modules.core.permissions.sync_cli
"""
import sys
import os
import logging
# Configurar logging básico para ver resultados en consola
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger(__name__)
# Asegurar que el backend esté en el path
sys.path.append(os.path.abspath("."))
sys.path.append(os.path.abspath("backend"))
from core.database import CoreSessionLocal
from api.v1.modules.core.permissions.service import PermissionService
def run_sync():
"""Ejecuta la lógica de sincronización modular."""
logger.info("Iniciando Sincronización Modular de Permisos...")
db = CoreSessionLocal()
try:
service = PermissionService(db)
result = service.sync_permissions()
logger.info("-" * 40)
logger.info(f"Sincronización Exitosa!")
logger.info(f" - Nuevos insertados: {result['synced']}")
logger.info(f" - Existentes actualizados: {result['updated']}")
logger.info(f" - Total en Registry: {result['total_registered']}")
logger.info("-" * 40)
except Exception as e:
logger.error(f"Error crítico durante la sincronización: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
finally:
db.close()
if __name__ == "__main__":
run_sync()

View File

@@ -52,7 +52,7 @@ def get_user_statistics(
- Usuarios disponibles
- Porcentaje de uso
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["user.view"])
service = UserService(db, tenant_id, company_id)
return service.get_user_stats()
@@ -71,7 +71,7 @@ def list_users(
Se puede filtrar por término de búsqueda (busca en username, email, nombre)
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["user.view"])
service = UserService(db, tenant_id, company_id)
result = service.get_tenant_users(page=page, page_size=page_size, search=search)
return result
@@ -245,7 +245,7 @@ def change_user_password(
- temporary=True: Usuario debe cambiar la contraseña en el próximo login
- temporary=False: Contraseña permanente
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, required_permissions=["user.update"])
service = UserService(db, tenant_id, company_id)
service.change_password(user_id, data.password, data.temporary)
return {"message": "Password changed successfully"}

View File

@@ -16,6 +16,7 @@ router = APIRouter(prefix="/code-pedimento-regimens")
def list_code_pedimento_regimens(
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(50, ge=1, le=1000, description="Tamaño de página"),
company_id: int = Query(..., description="ID de la empresa"),
code: str = Query(None, description="Filter by code"),
regime: str = Query(None, description="Filter by regime"),
type: str = Query(None, description="Filter by type"),
@@ -23,6 +24,8 @@ def list_code_pedimento_regimens(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["ref_pedimento_regimens.view"])
skip = (page - 1) * page_size
query = db.query(CodePedimentoRegimen)

View File

@@ -1,7 +1,7 @@
from typing import Any, Dict
from core.database import get_core_db
from core.security import get_current_user, has_role
from core.security import get_current_user, validate_access_to_resource
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import or_
@@ -16,10 +16,14 @@ router = APIRouter(prefix="/containers")
async def list_containers(
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"),
company_id: int = Query(..., description="ID de la empresa"),
search: str = Query(None, description="Término de búsqueda"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
# 🛡️ Permiso de Lectura (Listado)
validate_access_to_resource(db, company_id, current_user, required_permissions=["ref_containers.view"])
skip = (page - 1) * page_size
query = db.query(Container)
@@ -44,9 +48,13 @@ async def list_containers(
@router.get("/{key}", response_model=ContainerDTO)
async def get_container(
key: str,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
# 🛡️ Permiso de Lectura (Individual)
validate_access_to_resource(db, company_id, current_user, required_permissions=["ref_containers.view"])
obj = db.query(Container).filter(Container.key == key).first()
if not obj:
raise HTTPException(status_code=404, detail="Not found")
@@ -56,9 +64,13 @@ async def get_container(
@router.post("/", response_model=ContainerDTO, status_code=201)
async def create_container(
data: ContainerDTO,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(has_role("admin")),
current_user: dict = Depends(get_current_user),
):
# 🛡️ Permiso de Creación
validate_access_to_resource(db, company_id, current_user, required_permissions=["ref_containers.create"])
obj = Container(**data.dict())
db.add(obj)
db.commit()
@@ -70,9 +82,13 @@ async def create_container(
async def update_container(
key: str,
data: ContainerDTO,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(has_role("admin")),
current_user: dict = Depends(get_current_user),
):
# 🛡️ Permiso de Edición
validate_access_to_resource(db, company_id, current_user, required_permissions=["ref_containers.edit"])
obj = db.query(Container).filter(Container.key == key).first()
if not obj:
raise HTTPException(status_code=404, detail="Not found")
@@ -86,12 +102,16 @@ async def update_container(
@router.delete("/{key}", status_code=204)
async def delete_container(
key: str,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(has_role("admin")),
current_user: dict = Depends(get_current_user),
):
# 🛡️ Permiso de Borrado
validate_access_to_resource(db, company_id, current_user, required_permissions=["ref_containers.delete"])
obj = db.query(Container).filter(Container.key == key).first()
if not obj:
raise HTTPException(status_code=404, detail="Not found")
db.delete(obj)
db.commit()
return None
return None

View File

@@ -1,7 +1,7 @@
from typing import Any, Dict
from core.database import get_core_db
from core.security import get_current_user, has_role
from core.security import get_current_user
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
@@ -15,10 +15,14 @@ router = APIRouter(prefix="/countries")
async def list_countries(
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"),
company_id: int = Query(..., description="ID de la empresa"),
search: str = Query(None, description="Término de búsqueda"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Endpoint público para obtener lista de países - no requiere autenticación"""
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["ref_countries.view", "cat_countries.view"], require_all=False)
skip = (page - 1) * page_size
query = db.query(Country)
@@ -49,6 +53,8 @@ async def get_country(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource as validate_perm
validate_perm(current_user, "cat_countries", "view")
obj = db.query(Country).filter(Country.m3_key == m3_key).first()
if not obj:
raise HTTPException(status_code=404, detail="Not found")
@@ -59,8 +65,10 @@ async def get_country(
async def create_country(
data: CountryDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(has_role("admin")),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource as validate_perm
validate_perm(current_user, "cat_countries", "create")
obj = Country(**data.dict())
db.add(obj)
db.commit()
@@ -73,8 +81,10 @@ async def update_country(
m3_key: str,
data: CountryDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(has_role("admin")),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource as validate_perm
validate_perm(current_user, "cat_countries", "edit")
obj = db.query(Country).filter(Country.m3_key == m3_key).first()
if not obj:
raise HTTPException(status_code=404, detail="Not found")
@@ -89,8 +99,10 @@ async def update_country(
async def delete_country(
m3_key: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(has_role("admin")),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource as validate_perm
validate_perm(current_user, "cat_countries", "delete")
obj = db.query(Country).filter(Country.m3_key == m3_key).first()
if not obj:
raise HTTPException(status_code=404, detail="Not found")

View File

@@ -1,7 +1,7 @@
from typing import Any, Dict
from core.database import get_core_db
from core.security import get_current_user, has_role
from core.security import get_current_user
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import or_
@@ -16,10 +16,13 @@ router = APIRouter(prefix="/currency-types")
async def list_currency_types(
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"),
company_id: int = Query(..., description="ID de la empresa"),
search: str = Query(None, description="Término de búsqueda"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["ref_currency_types.view", "cat_currency.view"], require_all=False)
skip = (page - 1) * page_size
query = db.query(CurrencyType)
@@ -48,6 +51,8 @@ async def get_currency_type(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource as validate_perm
validate_perm(current_user, "cat_currency_types", "view")
obj = db.query(CurrencyType).filter(CurrencyType.code == code).first()
if not obj:
raise HTTPException(status_code=404, detail="Not found")
@@ -58,8 +63,10 @@ async def get_currency_type(
async def create_currency_type(
data: CurrencyTypeDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(has_role("admin")),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource as validate_perm
validate_perm(current_user, "cat_currency_types", "create")
obj = CurrencyType(**data.dict())
db.add(obj)
db.commit()
@@ -72,8 +79,10 @@ async def update_currency_type(
code: str,
data: CurrencyTypeDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(has_role("admin")),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource as validate_perm
validate_perm(current_user, "cat_currency_types", "edit")
obj = db.query(CurrencyType).filter(CurrencyType.code == code).first()
if not obj:
raise HTTPException(status_code=404, detail="Not found")
@@ -88,8 +97,10 @@ async def update_currency_type(
async def delete_currency_type(
code: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(has_role("admin")),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource as validate_perm
validate_perm(current_user, "cat_currency_types", "delete")
obj = db.query(CurrencyType).filter(CurrencyType.code == code).first()
if not obj:
raise HTTPException(status_code=404, detail="Not found")

View File

@@ -16,10 +16,13 @@ router = APIRouter(prefix="/customs-sections")
def list_customs_sections(
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"),
company_id: int = Query(..., description="ID de la empresa"),
search: str = Query(None, description="Término de búsqueda"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["ref_customs_sections.view"])
skip = (page - 1) * page_size
query = db.query(CustomsSection)
@@ -44,9 +47,12 @@ def list_customs_sections(
@router.get("/{customs_code}", response_model=CustomsSectionDTO)
def get_customs_section(
customs_code: str,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["ref_customs_sections.view"])
obj = (
db.query(CustomsSection)
.filter(CustomsSection.customs_code == customs_code)
@@ -60,9 +66,12 @@ def get_customs_section(
@router.post("/", response_model=CustomsSectionDTO, status_code=201)
def create_customs_section(
data: CustomsSectionDTO,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(has_role("admin")),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["ref_customs_sections.edit"])
obj = CustomsSection(**data.dict())
db.add(obj)
db.commit()
@@ -74,9 +83,12 @@ def create_customs_section(
def update_customs_section(
customs_code: str,
data: CustomsSectionDTO,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(has_role("admin")),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["ref_customs_sections.edit"])
obj = (
db.query(CustomsSection)
.filter(CustomsSection.customs_code == customs_code)
@@ -94,9 +106,12 @@ def update_customs_section(
@router.delete("/{customs_code}", status_code=204)
def delete_customs_section(
customs_code: str,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(has_role("admin")),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["ref_customs_sections.edit"])
obj = (
db.query(CustomsSection)
.filter(CustomsSection.customs_code == customs_code)

View File

@@ -16,10 +16,13 @@ router = APIRouter(prefix="/customs-warehouses")
def list_customs_warehouses(
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"),
company_id: int = Query(..., description="ID de la empresa"),
search: str = Query(None, description="Término de búsqueda"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["ref_customs_warehouses.view", "cat_warehouses.view"], require_all=False)
skip = (page - 1) * page_size
query = db.query(CustomsWarehouse)

View File

@@ -1,7 +1,7 @@
from typing import Any, Dict
from core.database import get_core_db
from core.security import get_current_user, has_role
from core.security import get_current_user
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import or_
from sqlalchemy.orm import Session
@@ -22,6 +22,8 @@ async def list_identifiers(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource as validate_perm
validate_perm(current_user, "cat_identifiers", "view")
skip = (page - 1) * page_size
query = db.query(IdentifierCatalog)
@@ -65,6 +67,8 @@ async def get_identifier(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource as validate_perm
validate_perm(current_user, "cat_identifiers", "view")
obj = db.query(IdentifierCatalog).filter(IdentifierCatalog.key == key).first()
if not obj:
raise HTTPException(status_code=404, detail="Not found")
@@ -75,8 +79,10 @@ async def get_identifier(
async def create_identifier(
data: IdentifierDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(has_role("admin")),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource as validate_perm
validate_perm(current_user, "cat_identifiers", "create")
# Check if already exists
existing = db.query(IdentifierCatalog).filter(IdentifierCatalog.key == data.key).first()
if existing:
@@ -94,8 +100,10 @@ async def update_identifier(
key: str,
data: IdentifierDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(has_role("admin")),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource as validate_perm
validate_perm(current_user, "cat_identifiers", "edit")
obj = db.query(IdentifierCatalog).filter(IdentifierCatalog.key == key).first()
if not obj:
raise HTTPException(status_code=404, detail="Not found")
@@ -110,8 +118,10 @@ async def update_identifier(
async def delete_identifier(
key: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(has_role("admin")),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource as validate_perm
validate_perm(current_user, "cat_identifiers", "delete")
obj = db.query(IdentifierCatalog).filter(IdentifierCatalog.key == key).first()
if not obj:
raise HTTPException(status_code=404, detail="Not found")

View File

@@ -16,12 +16,15 @@ router = APIRouter(prefix="/incoterms")
async def list_incoterms(
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"),
company_id: int = Query(..., description="ID de la empresa"),
code: str = Query(None, description="Filtrar por clave"),
description: str = Query(None, description="Filtrar por descripción"),
search: str = Query(None, description="Término de búsqueda"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["ref_incoterms.view", "cat_incoterms.view"], require_all=False)
skip = (page - 1) * page_size
query = db.query(Incoterm)

View File

@@ -16,11 +16,15 @@ router = APIRouter(prefix="/invoice-types")
def list_invoice_types(
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=100),
company_id: int = Query(..., description="ID de la empresa"),
type: Optional[str] = Query(None, description="Filter by type"),
operation: Optional[str] = Query(None, description="Filter by operation type (imp, exp, both)"),
search: str = Query(None, description="Término de búsqueda"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["ref_invoice_types.view", "cat_inv_types.view"], require_all=False)
query = db.query(InvoiceType)
if search:

View File

@@ -16,11 +16,14 @@ router = APIRouter(prefix="/material-types")
async def list_material_types(
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(50, ge=1, le=1000, description="Tamaño de página"),
company_id: int = Query(..., description="ID de la empresa"),
type: str = Query(None, description="Filtrar por tipo (ACTIVO FIJO, MATERIALES, PRODUCTOS)"),
search: str = Query(None, description="Término de búsqueda"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["ref_material_types.view", "cat_material_types.view"], require_all=False)
skip = (page - 1) * page_size
query = db.query(MaterialType)

View File

@@ -16,10 +16,13 @@ router = APIRouter(prefix="/payment-methods")
def list_payment_methods(
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"),
company_id: int = Query(..., description="ID de la empresa"),
search: str = Query(None, description="Término de búsqueda"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["pedimentos_payments.view"])
skip = (page - 1) * page_size
query = db.query(PaymentMethod)
@@ -44,9 +47,12 @@ def list_payment_methods(
@router.get("/{key}", response_model=PaymentMethodDTO)
def get_payment_method(
key: str,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["pedimentos_payments.view"])
obj = db.query(PaymentMethod).filter(PaymentMethod.key == key).first()
if not obj:
raise HTTPException(status_code=404, detail="Not found")

View File

@@ -16,10 +16,13 @@ router = APIRouter(prefix="/pedimento-codes")
def list_pedimento_codes(
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(50, ge=1, le=1000, description="Tamaño de página"),
company_id: int = Query(..., description="ID de la empresa"),
search: str = Query(None, description="Término de búsqueda"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["ref_pedimento_codes.view"])
skip = (page - 1) * page_size
query = db.query(PedimentoCode)
@@ -44,9 +47,12 @@ def list_pedimento_codes(
@router.get("/{code}", response_model=PedimentoCodeDTO)
def get_pedimento_code(
code: str,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["ref_pedimento_codes .view"])
obj = db.query(PedimentoCode).filter(PedimentoCode.code == code).first()
if not obj:
raise HTTPException(status_code=404, detail="Not found")

View File

@@ -1,7 +1,7 @@
from typing import Any, Dict
from core.database import get_core_db
from core.security import get_current_user, has_role
from core.security import get_current_user
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import or_
@@ -16,10 +16,13 @@ router = APIRouter(prefix="/pedimento-regimens")
def list_pedimento_regimens(
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"),
company_id: int = Query(..., description="ID de la empresa"),
search: str = Query(None, description="Término de búsqueda"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["ref_pedimento_regimens.view"])
skip = (page - 1) * page_size
query = db.query(RegimenPedimento)
@@ -44,9 +47,12 @@ def list_pedimento_regimens(
@router.get("/{key}", response_model=RegimenPedimentoDTO)
def get_pedimento_regimen(
key: str,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["ref_pedimento_regimens.view"])
obj = db.query(RegimenPedimento).filter(RegimenPedimento.code == key).first()
if not obj:
raise HTTPException(status_code=404, detail="Not found")
@@ -56,9 +62,13 @@ def get_pedimento_regimen(
@router.post("/", response_model=RegimenPedimentoDTO, status_code=201)
def create_pedimento_regimen(
data: RegimenPedimentoDTO,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(has_role("admin")),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
# Mutation for reference data usually restricted to admin role or specific perm
validate_access_to_resource(db, company_id, current_user, ["ref_pedimento_regimens.edit"])
obj = RegimenPedimento(**data.model_dump())
db.add(obj)
db.commit()
@@ -70,9 +80,12 @@ def create_pedimento_regimen(
def update_pedimento_regimen(
key: str,
data: RegimenPedimentoDTO,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(has_role("admin")),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["ref_pedimento_regimens.edit"])
obj = db.query(RegimenPedimento).filter(RegimenPedimento.code == key).first()
if not obj:
raise HTTPException(status_code=404, detail="Not found")
@@ -86,9 +99,12 @@ def update_pedimento_regimen(
@router.delete("/{key}", status_code=204)
def delete_pedimento_regimen(
key: str,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(has_role("admin")),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["ref_pedimento_regimens.edit"])
obj = db.query(RegimenPedimento).filter(RegimenPedimento.code == key).first()
if not obj:
raise HTTPException(status_code=404, detail="Not found")

View File

@@ -14,11 +14,15 @@ router = APIRouter(prefix="/pedimento-transport-catalog")
@router.get("/", response_model=Dict[str, Any])
async def list_pedimento_transport_catalog(
company_id: int = Query(..., description="ID de la empresa"),
page: int = Query(1, ge=1, description="Numero de pagina"),
page_size: int = Query(100, ge=1, le=200, description="Tamano de pagina"),
search: str = Query(None, description="Término de búsqueda"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["pedimentos_anexo22.view"])
skip = (page - 1) * page_size
query = db.query(PedimentoTransportCatalog)
@@ -43,7 +47,14 @@ async def list_pedimento_transport_catalog(
@router.get("/{code}", response_model=PedimentoTransportCatalogDTO)
async def get_pedimento_transport_catalog(code: str, db: Session = Depends(get_core_db)):
async def get_pedimento_transport_catalog(
code: str,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["pedimentos_anexo22.view"])
obj = (
db.query(PedimentoTransportCatalog)
.filter(PedimentoTransportCatalog.code == code)
@@ -57,9 +68,12 @@ async def get_pedimento_transport_catalog(code: str, db: Session = Depends(get_c
@router.post("/", response_model=PedimentoTransportCatalogDTO, status_code=201)
async def create_pedimento_transport_catalog(
data: PedimentoTransportCatalogDTO,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
user=Depends(get_current_user),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["pedimentos_anexo22.view"])
obj = PedimentoTransportCatalog(**data.model_dump())
db.add(obj)
db.commit()
@@ -71,9 +85,12 @@ async def create_pedimento_transport_catalog(
async def update_pedimento_transport_catalog(
code: str,
data: PedimentoTransportCatalogDTO,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
user=Depends(get_current_user),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["pedimentos_anexo22.view"])
obj = (
db.query(PedimentoTransportCatalog)
.filter(PedimentoTransportCatalog.code == code)
@@ -91,9 +108,12 @@ async def update_pedimento_transport_catalog(
@router.delete("/{code}", status_code=204)
async def delete_pedimento_transport_catalog(
code: str,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
user=Depends(get_current_user),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["pedimentos_anexo22.view"])
obj = (
db.query(PedimentoTransportCatalog)
.filter(PedimentoTransportCatalog.code == code)

View File

@@ -16,10 +16,13 @@ router = APIRouter(prefix="/states")
async def list_states(
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"),
company_id: int = Query(..., description="ID de la empresa"),
search: str = Query(None, description="Término de búsqueda"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["ref_states.view"])
skip = (page - 1) * page_size
query = db.query(State)

View File

@@ -1,7 +1,7 @@
from typing import Any, Dict
from core.database import get_core_db
from core.security import get_current_user
from core.security import get_current_user, validate_access_to_resource
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import or_
@@ -16,9 +16,14 @@ router = APIRouter(prefix="/transport-modes")
async def list_transport_modes(
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"),
company_id: int = Query(..., description="ID de la empresa"),
search: str = Query(None, description="Término de búsqueda"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
# 🛡️ Permiso de Lectura (Listado)
validate_access_to_resource(db, company_id, current_user, required_permissions=["ref_transport_modes.view"])
skip = (page - 1) * page_size
query = db.query(TransportMode)
@@ -41,7 +46,15 @@ async def list_transport_modes(
@router.get("/{key}", response_model=TransportModeDTO)
async def get_transport_mode(key: str, db: Session = Depends(get_core_db)):
async def get_transport_mode(
key: str,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
# 🛡️ Permiso de Lectura (Individual)
validate_access_to_resource(db, company_id, current_user, required_permissions=["ref_transport_modes.view"])
obj = db.query(TransportMode).filter(TransportMode.key == key).first()
if not obj:
raise HTTPException(status_code=404, detail="Not found")
@@ -51,9 +64,13 @@ async def get_transport_mode(key: str, db: Session = Depends(get_core_db)):
@router.post("/", response_model=TransportModeDTO, status_code=201)
async def create_transport_mode(
data: TransportModeDTO,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
user=Depends(get_current_user),
current_user: dict = Depends(get_current_user),
):
# 🛡️ Permiso de Creación
validate_access_to_resource(db, company_id, current_user, required_permissions=["ref_transport_modes.create"])
obj = TransportMode(**data.dict())
db.add(obj)
db.commit()
@@ -65,9 +82,13 @@ async def create_transport_mode(
async def update_transport_mode(
key: str,
data: TransportModeDTO,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
user=Depends(get_current_user),
current_user: dict = Depends(get_current_user),
):
# 🛡️ Permiso de Edición
validate_access_to_resource(db, company_id, current_user, required_permissions=["ref_transport_modes.edit"])
obj = db.query(TransportMode).filter(TransportMode.key == key).first()
if not obj:
raise HTTPException(status_code=404, detail="Not found")
@@ -80,11 +101,17 @@ async def update_transport_mode(
@router.delete("/{key}", status_code=204)
async def delete_transport_mode(
key: str, db: Session = Depends(get_core_db), user=Depends(get_current_user)
key: str,
company_id: int = Query(..., description="ID de la empresa"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
# 🛡️ Permiso de Borrado
validate_access_to_resource(db, company_id, current_user, required_permissions=["ref_transport_modes.delete"])
obj = db.query(TransportMode).filter(TransportMode.key == key).first()
if not obj:
raise HTTPException(status_code=404, detail="Not found")
db.delete(obj)
db.commit()
return None
return None

View File

@@ -16,9 +16,13 @@ router = APIRouter(prefix="/transport-types")
def list_transport_types(
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"),
company_id: int = Query(..., description="ID de la empresa"),
search: str = Query(None, description="Término de búsqueda"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["ref_transport_types.view"])
skip = (page - 1) * page_size
query = db.query(TransportType)

View File

@@ -16,10 +16,13 @@ router = APIRouter(prefix="/valuation-methods")
async def list_valuation_methods(
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"),
company_id: int = Query(..., description="ID de la empresa"),
search: str = Query(None, description="Término de búsqueda"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
from core.security import validate_access_to_resource
validate_access_to_resource(db, company_id, current_user, ["ref_valuation_methods.view", "cat_valuation.view"], require_all=False)
skip = (page - 1) * page_size
query = db.query(ValuationMethod)

View File

@@ -88,7 +88,7 @@ class LicenseValidationMiddleware(BaseHTTPMiddleware):
"""Middleware para validar la licencia del tenant antes de procesar requests."""
async def dispatch(self, request: Request, call_next: Callable):
if not settings.LICENSE_CHECK_ENABLED:
if not settings.LICENSE_CHECK_ENABLED or settings.ENVIRONMENT == "development":
return await call_next(request)
exempt_paths = [

View File

@@ -207,43 +207,83 @@ def validate_access_to_resource(
tenant_id = get_tenant_from_token(current_user)
if not tenant_id:
# Fallback para desarrollo o tokens mal formados que sí tienen el atributo pero en otro lado
# Esto evita el 400 si get_tenant_from_token falla pero el usuario es válido
tenant_id = current_user.get("tenant_id")
# 🕵️ DEBUG ULTRA-DETALLADO (Ver en consola del backend)
print("--- TOKEN DEBUG START ---")
print(f"Usuario: {current_user.get('preferred_username')}")
print(f"Sub: {current_user.get('sub')}")
print(f"Realm Roles: {current_user.get('realm_access', {}).get('roles', [])}")
print(f"Resource Access: {current_user.get('resource_access', {})}")
print("--- TOKEN DEBUG END ---")
# 🛡️ DETERMINAR SI ES ADMIN DE KEYCLOAK
realm_roles = current_user.get("realm_access", {}).get("roles", [])
# Buscamos en todos los clientes posibles por si acaso
all_client_roles = []
for client in current_user.get("resource_access", {}).values():
all_client_roles.extend(client.get("roles", []))
all_user_roles = set(realm_roles + all_client_roles)
is_keycloak_admin = "admin" in all_user_roles
# 🚪 EXCEPCIÓN ESPECIAL: Si es el endpoint /me, permitimos el paso para el Bootstrap
# Detectamos si no se requieren permisos (típico de /me)
is_me_endpoint = required_permissions is None
if not is_keycloak_admin and not is_me_endpoint:
if not validate_company_access(db, company_id, current_user):
print(f"DEBUG: Acceso denegado a compañía {company_id}")
raise HTTPException(status_code=403, detail="Access denied to this company")
# Si no hay tenant_id, intentamos recuperarlo de la empresa
if not tenant_id:
try:
from api.v1.modules.a76.general_catalogs.company.models import Company
company = db.query(Company).filter(Company.id == company_id).first()
if company:
tenant_id = company.tenant_id
except:
pass
# Si aún no hay tenant_id y no es admin, error 400
if not tenant_id and not is_keycloak_admin and not is_me_endpoint:
raise HTTPException(status_code=400, detail="Tenant ID not found in token")
if not validate_company_access(db, company_id, current_user):
raise HTTPException(status_code=403, detail="Access denied to this company")
# Verificar permisos si se proporcionaron
# Verificar permisos locales
if required_permissions:
if is_keycloak_admin:
return tenant_id or 1
from api.v1.modules.core.permissions.service import PermissionService
user_id = current_user.get("sub") or current_user.get("id")
if not user_id:
raise HTTPException(status_code=401, detail="User ID not found in token")
permission_service = PermissionService(db)
has_access = False
if require_all:
has_access = permission_service.has_all_permissions(
user_id=user_id,
company_id=company_id,
permission_codes=required_permissions,
)
has_access = permission_service.has_all_permissions(user_id, company_id, required_permissions)
else:
has_access = permission_service.has_any_permission(
user_id=user_id,
company_id=company_id,
permission_codes=required_permissions,
)
has_access = permission_service.has_any_permission(user_id, company_id, required_permissions)
# 🛡️ MEJORA DEV: Auto-bootstrap si falla el acceso en desarrollo
if not has_access and settings.ENVIRONMENT == "development":
try:
# Si el usuario no tiene roles asignados, intentamos el bootstrap
# bootstrap_super_admin solo asigna el rol si no tiene ninguno (o es admin)
permission_service.bootstrap_super_admin(user_id, company_id)
# Re-validar
if require_all:
has_access = permission_service.has_all_permissions(user_id, company_id, required_permissions)
else:
has_access = permission_service.has_any_permission(user_id, company_id, required_permissions)
if has_access:
print(f"DEBUG: Auto-bootstrap exitoso para {user_id} en empresa {company_id}")
except Exception as e:
print(f"DEBUG: Error en auto-bootstrap de seguridad: {e}")
if not has_access:
raise HTTPException(
status_code=403,
detail=f"Missing required permissions: {', '.join(required_permissions)}",
)
print(f"DEBUG: Permiso denegado. Faltan: {required_permissions}")
raise HTTPException(status_code=403, detail="Permission denied")
return tenant_id
return tenant_id or 1

View File

@@ -1,4 +1,4 @@
import { BACKEND_URL } from '$lib/config/backend';
import { api } from '$lib/api';
export interface AppSettingsRequest {
tenant_id?: number | null;
@@ -11,24 +11,17 @@ export const appSettingsApi = {
* Resolves settings merging Global -> Tenant -> Company hierarchy
*/
async getResolved(tenantId: number, companyId: number): Promise<Record<string, any>> {
const response = await fetch(`${BACKEND_URL}/v1/a76/app-settings/resolved?tenant_id=${tenantId}&company_id=${companyId}`);
if (!response.ok) throw new Error('Error al obtener configuraciones');
return response.json();
const res = await api.get<Record<string, any>>(`/v1/a76/app-settings/resolved?tenant_id=${tenantId}&company_id=${companyId}`);
if (res.error) throw new Error(res.error);
return res.data || {};
},
/**
* Upserts an override at a specific level
*/
async upsert(payload: AppSettingsRequest): Promise<any> {
const response = await fetch(`${BACKEND_URL}/v1/a76/app-settings/upsert`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Error al guardar configuración');
}
return response.json();
const res = await api.post<any>(`/v1/a76/app-settings/upsert`, payload);
if (res.error) throw new Error(res.error);
return res.data;
}
};

View File

@@ -63,4 +63,15 @@ export async function updateClassificationConcept(
export async function deleteClassificationConcept(id: number, companyId: number): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/classification-concepts/${id}?company_id=${companyId}`);
}
}
export const classificationApi = {
list: (companyId: number, options: Record<string, any> = {}) => {
const { page = '1', page_size = '50', ...filters } = options;
return getClassificationConcepts(Number(page), Number(page_size), companyId, filters);
},
get: (id: number, companyId: number) => getClassificationConcept(id, companyId),
create: (data: ClassificationConceptCreate, companyId: number) => createClassificationConcept(data, companyId),
update: (id: number, data: ClassificationConceptUpdate, companyId: number) => updateClassificationConcept(id, data, companyId),
delete: (id: number, companyId: number) => deleteClassificationConcept(id, companyId)
};

View File

@@ -61,3 +61,16 @@ export async function updateCustomsBrokerConcept(id: number, data: CustomsBroker
export async function deleteCustomsBrokerConcept(id: number, companyId: number): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/customs-broker-concepts/${id}?company_id=${companyId}`);
}
export const customsBrokerConceptsApi = {
list: (page = 1, pageSize = 50, companyId: number, filters: any = {}) =>
getCustomsBrokerConcepts(page, pageSize, companyId, filters),
get: (id: number, companyId: number) =>
getCustomsBrokerConcept(id, companyId),
create: (data: CustomsBrokerConceptCreate, companyId: number) =>
createCustomsBrokerConcept(data, companyId),
update: (id: number, data: CustomsBrokerConceptUpdate, companyId: number) =>
updateCustomsBrokerConcept(id, data, companyId),
delete: (id: number, companyId: number) =>
deleteCustomsBrokerConcept(id, companyId)
};

View File

@@ -178,10 +178,22 @@ export async function getDodas(
if (companyId) {
params.append('company_id', companyId.toString());
}
const response = await api.get(`/v1/a76/doda/?${params.toString()}`);
return response.data;
return await api.get(`/v1/a76/doda/?${params.toString()}`);
}
export const dodaApi = {
list: (page = 1, pageSize = 50, companyId: number, filters: any = {}) =>
getDodas(page, pageSize, filters, companyId),
get: (id: number, companyId: number) =>
api.get<Doda>(`/v1/a76/doda/${id}/detail?company_id=${companyId}`),
create: (data: DodaCreate, companyId: number) =>
api.post<Doda>(`/v1/a76/doda/?company_id=${companyId}`, data),
update: (id: number, data: DodaUpdate, companyId: number) =>
api.put<Doda>(`/v1/a76/doda/${id}/?company_id=${companyId}`, data),
delete: (id: number, companyId: number) =>
api.delete(`/v1/a76/doda/${id}/?company_id=${companyId}`)
};
export async function getDoda(id: number, companyId?: number): Promise<Doda> {
const params = new URLSearchParams();
if (companyId) {

View File

@@ -113,4 +113,15 @@ export async function deleteElectronicNotice(id: number, companyId: number): Pro
if (response.error) {
throw new Error(response.error);
}
}
}
export const electronicNoticesApi = {
list: (companyId: number, options: Record<string, any> = {}) => {
const { page = '1', page_size = '50', ...filters } = options;
return getElectronicNotices(Number(page), Number(page_size), filters, companyId) as any;
},
get: (id: number, companyId: number) => getElectronicNotice(id, companyId),
create: (data: ElectronicNoticeCreate, companyId: number) => createElectronicNotice(data, companyId),
update: (id: number, data: ElectronicNoticeUpdate, companyId: number) => updateElectronicNotice(id, data, companyId),
delete: (id: number, companyId: number) => deleteElectronicNotice(id, companyId)
};

View File

@@ -1,4 +1,4 @@
import { api } from '$lib/api';
import { api, type ApiResponse } from '$lib/api';
// ==========================================
// ERROR CLASSIFICATION
@@ -67,7 +67,7 @@ export async function getErrorClassifications(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ErrorClassificationListResponse> {
): Promise<ApiResponse<ErrorClassificationListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
@@ -75,31 +75,27 @@ export async function getErrorClassifications(
...filters
});
const response = await api.get(`/v1/a76/error-catalogs/classifications/?${params.toString()}`);
return response.data;
return await api.get(`/v1/a76/error-catalogs/classifications/?${params.toString()}`);
}
export async function getErrorClassification(id: number, companyId: number): Promise<ErrorClassification> {
export async function getErrorClassification(id: number, companyId: number): Promise<ApiResponse<ErrorClassification>> {
const params = new URLSearchParams({ company_id: companyId.toString() });
const response = await api.get(`/v1/a76/error-catalogs/classifications/${id}?${params.toString()}`);
return response.data;
return await api.get(`/v1/a76/error-catalogs/classifications/${id}?${params.toString()}`);
}
export async function createErrorClassification(data: ErrorClassificationCreate, companyId: number): Promise<ErrorClassification> {
export async function createErrorClassification(data: ErrorClassificationCreate, companyId: number): Promise<ApiResponse<ErrorClassification>> {
const params = new URLSearchParams({ company_id: companyId.toString() });
const response = await api.post(`/v1/a76/error-catalogs/classifications/?${params.toString()}`, data);
return response.data;
return await api.post(`/v1/a76/error-catalogs/classifications/?${params.toString()}`, data);
}
export async function updateErrorClassification(id: number, data: ErrorClassificationUpdate, companyId: number): Promise<ErrorClassification> {
export async function updateErrorClassification(id: number, data: ErrorClassificationUpdate, companyId: number): Promise<ApiResponse<ErrorClassification>> {
const params = new URLSearchParams({ company_id: companyId.toString() });
const response = await api.put(`/v1/a76/error-catalogs/classifications/${id}/?${params.toString()}`, data);
return response.data;
return await api.put(`/v1/a76/error-catalogs/classifications/${id}/?${params.toString()}`, data);
}
export async function deleteErrorClassification(id: number, companyId: number): Promise<void> {
export async function deleteErrorClassification(id: number, companyId: number): Promise<ApiResponse<void>> {
const params = new URLSearchParams({ company_id: companyId.toString() });
await api.delete(`/v1/a76/error-catalogs/classifications/${id}?${params.toString()}`);
return await api.delete(`/v1/a76/error-catalogs/classifications/${id}?${params.toString()}`);
}
// --- Catalogs ---
@@ -109,7 +105,7 @@ export async function getErrorCatalogs(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ErrorCatalogListResponse> {
): Promise<ApiResponse<ErrorCatalogListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
@@ -117,29 +113,33 @@ export async function getErrorCatalogs(
...filters
});
const response = await api.get(`/v1/a76/error-catalogs/?${params.toString()}`);
return response.data;
return await api.get(`/v1/a76/error-catalogs/?${params.toString()}`);
}
export async function getErrorCatalog(id: number, companyId: number): Promise<ErrorCatalog> {
export async function getErrorCatalog(id: number, companyId: number): Promise<ApiResponse<ErrorCatalog>> {
const params = new URLSearchParams({ company_id: companyId.toString() });
const response = await api.get(`/v1/a76/error-catalogs/${id}?${params.toString()}`);
return response.data;
return await api.get(`/v1/a76/error-catalogs/${id}?${params.toString()}`);
}
export async function createErrorCatalog(data: ErrorCatalogCreate, companyId: number): Promise<ErrorCatalog> {
export async function createErrorCatalog(data: ErrorCatalogCreate, companyId: number): Promise<ApiResponse<ErrorCatalog>> {
const params = new URLSearchParams({ company_id: companyId.toString() });
const response = await api.post(`/v1/a76/error-catalogs/?${params.toString()}`, data);
return response.data;
return await api.post(`/v1/a76/error-catalogs/?${params.toString()}`, data);
}
export async function updateErrorCatalog(id: number, data: ErrorCatalogUpdate, companyId: number): Promise<ErrorCatalog> {
export async function updateErrorCatalog(id: number, data: ErrorCatalogUpdate, companyId: number): Promise<ApiResponse<ErrorCatalog>> {
const params = new URLSearchParams({ company_id: companyId.toString() });
const response = await api.put(`/v1/a76/error-catalogs/${id}/?${params.toString()}`, data);
return response.data;
return await api.put(`/v1/a76/error-catalogs/${id}/?${params.toString()}`, data);
}
export async function deleteErrorCatalog(id: number, companyId: number): Promise<void> {
export async function deleteErrorCatalog(id: number, companyId: number): Promise<ApiResponse<void>> {
const params = new URLSearchParams({ company_id: companyId.toString() });
await api.delete(`/v1/a76/error-catalogs/${id}?${params.toString()}`);
}
return await api.delete(`/v1/a76/error-catalogs/${id}?${params.toString()}`);
}
export const errorCatalogsApi = {
list: (companyId: number, options: any = {}) => getErrorCatalogs(companyId, Number(options.page || 1), Number(options.page_size || 50), options),
get: (id: number, companyId: number) => getErrorCatalog(id, companyId),
create: (data: any, companyId: number) => createErrorCatalog(data, companyId),
update: (id: number, data: any, companyId: number) => updateErrorCatalog(id, data, companyId),
delete: (id: number, companyId: number) => deleteErrorCatalog(id, companyId)
};

View File

@@ -92,6 +92,18 @@ export async function deleteExchangeRate(
return api.delete(`/v1/a76/exchange-rate/${exchangeRateId}?${params.toString()}`);
}
export const exchangeRateApi = {
list: (companyId: number, options: Record<string, any> = {}) => {
const { page = 1, page_size = 50, ...filters } = options;
return getExchangeRates(companyId, { ...filters, page: Number(page), page_size: Number(page_size) });
},
get: (id: number, companyId: number) => getExchangeRate(id, companyId),
create: (data: ExchangeRateCreate, companyId: number) => createExchangeRate(data, companyId),
update: (id: number, data: ExchangeRateUpdate, companyId: number) => updateExchangeRate(id, data, companyId),
delete: (id: number, companyId: number) => deleteExchangeRate(id, companyId),
getDof: (date: string) => getDofExchangeRate(date)
};
export interface DofResponse {
success: boolean;
message?: string;

View File

@@ -123,4 +123,21 @@ export async function deleteIdentifierDetail(
companyId: number
): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/identifiers/details/${id}?company_id=${companyId}`);
}
}
export const identifiersApi = {
list: (companyId: number, options: Record<string, any> = {}) => {
const { page = '1', page_size = '50', ...filters } = options;
return getIdentifiers(Number(page), Number(page_size), companyId, filters);
},
get: (id: number, companyId: number) => {
return api.get(`/v1/a76/identifiers/${id}?company_id=${companyId}`);
},
create: (data: IdentifierCreate, companyId: number) => createIdentifier(data, companyId),
update: (id: number, data: IdentifierUpdate, companyId: number) => updateIdentifier(id, data, companyId),
delete: (id: number, companyId: number) => deleteIdentifier(id, companyId),
// Details
createDetail: (data: IdentifierDetailCreate, companyId: number) => createIdentifierDetail(data, companyId),
updateDetail: (id: number, data: IdentifierDetailUpdate, companyId: number) => updateIdentifierDetail(id, data, companyId),
deleteDetail: (id: number, companyId: number) => deleteIdentifierDetail(id, companyId)
};

View File

@@ -66,3 +66,14 @@ export async function updateINPC(
export async function deleteINPC(id: number, companyId: number): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/inpc/${id}?company_id=${companyId}`);
}
export const inpcApi = {
list: (companyId: number, options: Record<string, any> = {}) => {
const { page = '1', page_size = '50', ...filters } = options;
return getINPCs(Number(page), Number(page_size), companyId, filters);
},
get: (id: number, companyId: number) => getINPC(id, companyId),
create: (data: INPCCreate, companyId: number) => createINPC(data, companyId),
update: (id: number, data: INPCUpdate, companyId: number) => updateINPC(id, data, companyId),
delete: (id: number, companyId: number) => deleteINPC(id, companyId)
};

View File

@@ -62,4 +62,16 @@ export async function updateLegend(
export async function deleteLegend(id: number, companyId: number): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/legends/${id}?company_id=${companyId}`);
}
}
// Wrapper object to match the rest of the codebase API style (e.g. `legendsApi.list(...)`)
export const legendsApi = {
list: (companyId: number, options: Record<string, any> | undefined = {}) => {
const { page = '1', page_size = '50', ...filters } = options as any;
return getLegends(Number(page), Number(page_size), companyId, filters);
},
get: (id: number, companyId: number) => getLegend(id, companyId),
create: (data: LegendCreate, companyId: number) => createLegend(data, companyId),
update: (id: number, data: LegendUpdate, companyId: number) => updateLegend(id, data, companyId),
delete: (id: number, companyId: number) => deleteLegend(id, companyId)
};

View File

@@ -98,3 +98,14 @@ export async function deleteLocation(
const q = new URLSearchParams({ company_id: String(companyId) });
await api.delete(`${BASE_URL}/${locationId}?${q}`);
}
export const locationsApi = {
list: (companyId: number, options: Record<string, any> = {}) => {
const { page = 1, page_size = 50, ...filters } = options;
return getLocations(companyId, { ...filters, page: Number(page), page_size: Number(page_size) });
},
get: (id: number, companyId: number) => getLocation(id, companyId),
create: (data: LocationCreate, companyId: number) => createLocation(data, companyId),
update: (id: number, data: LocationUpdate, companyId: number) => updateLocation(id, data, companyId),
delete: (id: number, companyId: number) => deleteLocation(id, companyId)
};

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