fix: solución de bloqueos y estandarización de permisos
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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,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, 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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user