fix: solución de bloqueos y estandarización de permisos

This commit is contained in:
2026-04-27 10:47:30 -05:00
parent fa3cbb4d0a
commit 5c2a84e95d
312 changed files with 13889 additions and 7911 deletions

View File

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

View File

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

View File

@@ -42,7 +42,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"])
query = db.query(ClientProvider).options(
joinedload(ClientProvider.address),
@@ -83,7 +83,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:
@@ -100,7 +100,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)
@@ -114,7 +114,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
@@ -135,7 +135,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)
@@ -153,7 +153,7 @@ async def delete_client_provider(
current_user: dict = Depends(get_current_user),
):
"""Delete a client/provider"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
tenant_id = validate_access_to_resource(db, company_id, current_user, ["partners_mgmt.delete"])
success = ClientProviderService.delete(db, client_id, tenant_id, company_id)
if not success:

View File

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

View File

@@ -11,4 +11,9 @@ 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"],
).router

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ from fastapi import APIRouter, Depends, Query
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,
@@ -36,6 +36,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:
@@ -74,9 +80,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
@@ -107,6 +116,7 @@ async def create_tariff_fraction(
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
@@ -159,6 +169,7 @@ async def update_tariff_fraction(
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
@@ -204,6 +215,7 @@ async def delete_tariff_fraction(
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:

View File

@@ -28,6 +28,10 @@ crud_router = TenantCRUDRoutes(
resource_name="US Tariff Fraction",
id_name="id",
enable_list=False, # We implement our custom list endpoint
create_permissions=["frac_american.create"],
update_permissions=["frac_american.edit"],
delete_permissions=["frac_american.delete"],
get_permissions=["frac_american.view"],
)
# Master router with prefix so all routes live under /us-tariff-fractions
@@ -52,6 +56,8 @@ 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)
skip = (page - 1) * page_size

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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