Merge pull request 'fix/permisos' (#338) from fix/permisos into development
Reviewed-on: ADUANASOFT/anexo76#338
This commit is contained in:
@@ -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
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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"}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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).",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
65
backend/api/v1/modules/core/permissions/cleanup_cli.py
Normal file
65
backend/api/v1/modules/core/permissions/cleanup_cli.py
Normal 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()
|
||||
101
backend/api/v1/modules/core/permissions/registry.py
Normal file
101
backend/api/v1/modules/core/permissions/registry.py
Normal 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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
]
|
||||
232
backend/api/v1/modules/core/permissions/seed_v2.py
Normal file
232
backend/api/v1/modules/core/permissions/seed_v2.py
Normal 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()
|
||||
@@ -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
|
||||
|
||||
50
backend/api/v1/modules/core/permissions/sync_cli.py
Normal file
50
backend/api/v1/modules/core/permissions/sync_cli.py
Normal 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()
|
||||
@@ -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"}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
@@ -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)
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
@@ -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)
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
@@ -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)
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
@@ -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
Reference in New Issue
Block a user