From e3ad7e7a7e4f7459d70c3d9bae5f2f20fdd51ebd Mon Sep 17 00:00:00 2001 From: hreyes Date: Wed, 6 May 2026 11:13:09 -0600 Subject: [PATCH] feature/catalogos-generales-permisos --- .../a76/general_catalogs/company/routes.py | 61 ++++++++++++++- .../depreciation_catalog/routes.py | 8 +- .../a76/general_catalogs/doda/routes.py | 74 ++++++++++++++++--- .../general_catalogs/error_catalogs/routes.py | 44 ++++++++--- .../canadian_tariff_fractions/routes.py | 30 ++++---- .../historical_tariff_fractions/routes.py | 36 ++++----- .../fractions/tariff_fractions/routes.py | 15 +++- .../fractions/us_tariff_fractions/routes.py | 8 +- .../a76/general_catalogs/location/routes.py | 5 ++ .../general_catalogs/prevalidators/routes.py | 30 ++++++-- .../a76/general_catalogs/signatures/routes.py | 12 ++- .../v1/modules/core/permissions/seed_v2.py | 11 +-- .../doda/doda-form-modal.svelte | 20 ++++- .../locations/create-edit-dialog.svelte | 14 +++- .../sectors/SectorsList.svelte | 11 +-- .../components/dashboard/locations/columns.ts | 10 ++- .../locations/data-table-actions.svelte | 13 +++- .../locations/locations-catalog.svelte | 33 +++++++-- .../src/lib/components/sidebar/modules.ts | 30 ++++++-- .../catalogs/broker-concepts-permissions.ts | 12 +++ .../classification-concepts-permissions.ts | 12 +++ .../catalogs/company-catalog-permissions.ts | 12 +++ .../catalogs/concepts-permissions.ts | 12 +++ .../depreciation-catalog-permissions.ts | 15 ++++ .../permissions/catalogs/doda-permissions.ts | 12 +++ .../catalogs/drivers-permissions.ts | 11 +++ .../electronic-notices-permissions.ts | 12 +++ .../catalogs/equivalencies-permissions.ts | 12 +++ .../catalogs/error-catalogs-permissions.ts | 12 +++ .../catalogs/exchange-rates-permissions.ts | 12 +++ .../catalogs/identifiers-permissions.ts | 12 +++ .../permissions/catalogs/inpc-permissions.ts | 12 +++ .../catalogs/legends-permissions.ts | 12 +++ .../catalogs/locations-catalog-permissions.ts | 12 +++ .../multi-currency-types-permissions.ts | 12 +++ .../catalogs/packages-permissions.ts | 12 +++ .../permissions/catalogs/ports-permissions.ts | 12 +++ .../catalogs/prevalidators-permissions.ts | 12 +++ .../permissions/catalogs/seals-permissions.ts | 12 +++ .../catalogs/sectors-catalog-permissions.ts | 12 +++ .../catalogs/signatures-permissions.ts | 12 +++ .../catalogs/trailers-permissions.ts | 11 +++ .../catalogs/transporters-permissions.ts | 14 ++++ .../catalogs/um-ace-permissions.ts | 12 +++ .../catalogs/um-american-permissions.ts | 12 +++ .../catalogs/um-customs-permissions.ts | 12 +++ .../catalogs/um-general-permissions.ts | 12 +++ .../catalogs/um-oma-permissions.ts | 12 +++ .../catalogs/unit-conversions-permissions.ts | 12 +++ .../catalogs/vehicles-permissions.ts | 11 +++ .../containers-ref-permissions.ts | 13 ++++ .../countries-ref-permissions.ts | 13 ++++ .../currency-types-ref-permissions.ts | 13 ++++ .../customs-sections-ref-permissions.ts | 13 ++++ .../customs-warehouses-ref-permissions.ts | 13 ++++ .../incoterms-ref-permissions.ts | 13 ++++ .../invoice-types-ref-permissions.ts | 13 ++++ .../material-types-ref-permissions.ts | 13 ++++ .../payment-methods-ref-permissions.ts | 13 ++++ .../pedimento-codes-ref-permissions.ts | 13 ++++ .../pedimento-regimens-ref-permissions.ts | 13 ++++ .../reference-data/sectors-ref-permissions.ts | 13 ++++ .../reference-data/states-ref-permissions.ts | 13 ++++ .../transport-modes-ref-permissions.ts | 13 ++++ .../transport-types-ref-permissions.ts | 13 ++++ .../valuation-methods-ref-permissions.ts | 13 ++++ .../lib/permissions/transport-permissions.ts | 19 ----- .../dashboard/despacho/doda/+page.svelte | 35 +++++++-- .../classification_concepts/+page.svelte | 12 +-- .../company_information/+page.svelte | 18 +++-- .../edit/[[id]]/+page.svelte | 34 ++++++++- .../general_catalogs/concepts/+page.svelte | 12 +-- .../customs_broker_concepts/+page.svelte | 14 ++-- .../general_catalogs/doda/+page.svelte | 12 +-- .../doda/edit/[[id]]/+page.svelte | 33 ++++++++- .../general_catalogs/drivers/+page.svelte | 14 ++-- .../electronic_notices/+page.svelte | 14 ++-- .../equivalencies/+page.svelte | 12 +-- .../error_catalogs/+page.svelte | 14 ++-- .../exchange-rate/+page.svelte | 12 +-- .../exchange-rate/new/+page.svelte | 22 +++++- .../general_catalogs/identifiers/+page.svelte | 12 +-- .../general_catalogs/inpc/+page.svelte | 12 +-- .../general_catalogs/legends/+page.svelte | 12 +-- .../general_catalogs/locations/+page.svelte | 5 ++ .../multi_currency_types/+page.svelte | 12 +-- .../general_catalogs/packages/+page.svelte | 12 +-- .../general_catalogs/ports/+page.svelte | 12 +-- .../prevalidators/+page.svelte | 13 ++-- .../general_catalogs/seal/+page.svelte | 12 +-- .../general_catalogs/signatures/+page.svelte | 12 +-- .../general_catalogs/trailers/+page.svelte | 14 ++-- .../transporters/+page.svelte | 14 ++-- .../unit_conversions/+page.svelte | 12 +-- .../units_of_measure/ace/+page.svelte | 12 +-- .../units_of_measure/american/+page.svelte | 12 +-- .../units_of_measure/customs/+page.svelte | 12 +-- .../units_of_measure/general/+page.svelte | 12 +-- .../units_of_measure/oma/+page.svelte | 12 +-- .../general_catalogs/vehicles/+page.svelte | 14 ++-- .../code_pedimento_regimens/+page.svelte | 5 +- .../reference_data/containers/+page.svelte | 5 +- .../reference_data/countries/+page.svelte | 5 +- .../currency_types/+page.svelte | 5 +- .../customs_sections/+page.svelte | 5 +- .../customs_warehouses/+page.svelte | 5 +- .../document_types_digitization/+page.svelte | 5 +- .../reference_data/incoterms/+page.svelte | 5 +- .../reference_data/invoice_types/+page.svelte | 5 +- .../material_types/+page.svelte | 5 +- .../payment_methods/+page.svelte | 5 +- .../pedimento_codes/+page.svelte | 5 +- .../pedimento_regimens/+page.svelte | 5 +- .../reference_data/sectors/+page.svelte | 5 +- .../reference_data/states/+page.svelte | 5 +- .../transport_modes/+page.svelte | 5 +- .../transport_types/+page.svelte | 5 +- .../valuation_methods/+page.svelte | 5 +- 118 files changed, 1318 insertions(+), 324 deletions(-) create mode 100644 frontend/src/lib/permissions/catalogs/broker-concepts-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/classification-concepts-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/company-catalog-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/concepts-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/depreciation-catalog-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/doda-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/drivers-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/electronic-notices-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/equivalencies-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/error-catalogs-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/exchange-rates-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/identifiers-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/inpc-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/legends-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/locations-catalog-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/multi-currency-types-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/packages-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/ports-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/prevalidators-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/seals-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/sectors-catalog-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/signatures-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/trailers-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/transporters-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/um-ace-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/um-american-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/um-customs-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/um-general-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/um-oma-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/unit-conversions-permissions.ts create mode 100644 frontend/src/lib/permissions/catalogs/vehicles-permissions.ts create mode 100644 frontend/src/lib/permissions/reference-data/containers-ref-permissions.ts create mode 100644 frontend/src/lib/permissions/reference-data/countries-ref-permissions.ts create mode 100644 frontend/src/lib/permissions/reference-data/currency-types-ref-permissions.ts create mode 100644 frontend/src/lib/permissions/reference-data/customs-sections-ref-permissions.ts create mode 100644 frontend/src/lib/permissions/reference-data/customs-warehouses-ref-permissions.ts create mode 100644 frontend/src/lib/permissions/reference-data/incoterms-ref-permissions.ts create mode 100644 frontend/src/lib/permissions/reference-data/invoice-types-ref-permissions.ts create mode 100644 frontend/src/lib/permissions/reference-data/material-types-ref-permissions.ts create mode 100644 frontend/src/lib/permissions/reference-data/payment-methods-ref-permissions.ts create mode 100644 frontend/src/lib/permissions/reference-data/pedimento-codes-ref-permissions.ts create mode 100644 frontend/src/lib/permissions/reference-data/pedimento-regimens-ref-permissions.ts create mode 100644 frontend/src/lib/permissions/reference-data/sectors-ref-permissions.ts create mode 100644 frontend/src/lib/permissions/reference-data/states-ref-permissions.ts create mode 100644 frontend/src/lib/permissions/reference-data/transport-modes-ref-permissions.ts create mode 100644 frontend/src/lib/permissions/reference-data/transport-types-ref-permissions.ts create mode 100644 frontend/src/lib/permissions/reference-data/valuation-methods-ref-permissions.ts delete mode 100644 frontend/src/lib/permissions/transport-permissions.ts create mode 100644 frontend/src/routes/dashboard/general_catalogs/locations/+page.svelte diff --git a/backend/api/v1/modules/a76/general_catalogs/company/routes.py b/backend/api/v1/modules/a76/general_catalogs/company/routes.py index 10c1d5c6..00191e97 100644 --- a/backend/api/v1/modules/a76/general_catalogs/company/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/company/routes.py @@ -19,6 +19,8 @@ from core.database import get_core_db from core.s3_keys import company_certificate_key, company_logo_key from core.storage_s3 import delete_object_if_exists, get_object_bytes, put_object_bytes from core.security import ( + collect_company_ids_from_app_membership, + collect_user_role_names, get_current_user, get_tenant_from_token, resolve_effective_tenant_id_from_user, @@ -37,6 +39,48 @@ MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB logger = logging.getLogger(__name__) +def _user_is_admin(current_user: dict) -> bool: + return "admin" in collect_user_role_names(current_user) + + +def _assert_permission_any_company( + db: Session, + current_user: dict, + permission_code: str, +) -> None: + """ + Enforces a permission when endpoint has no explicit company_id param. + """ + if _user_is_admin(current_user): + return + + user_id = current_user.get("sub") or current_user.get("id") + if not user_id: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Permission denied") + + company_ids = collect_company_ids_from_app_membership(db, str(user_id)) + if not company_ids: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Permission denied") + + from api.v1.modules.core.permissions.service import PermissionService + + permission_service = PermissionService(db) + for company_id in company_ids: + if permission_service.has_any_permission(str(user_id), int(company_id), [permission_code]): + return + + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Permission denied") + + +def _assert_permission_for_company( + db: Session, + company_id: int, + current_user: dict, + permission_code: str, +) -> int: + return validate_access_to_resource(db, company_id, current_user, [permission_code]) + + def _resolve_tenant_id_int(current_user: dict) -> int: """Misma lógica que validate_access_to_resource: entero estable para BD y claves S3.""" tid = get_tenant_from_token(current_user) @@ -87,6 +131,7 @@ async def create_company( db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user), ): + _assert_permission_any_company(db, current_user, "cat_company.create") tenant_id = current_user.get("tenant_id") if not tenant_id: @@ -114,6 +159,8 @@ async def list_companies( current_user: dict = Depends(get_current_user), ): """Get paginated list of companies for current tenant with optional filters""" + _assert_permission_any_company(db, current_user, "cat_company.view") + tenant_id = current_user.get("tenant_id") if not tenant_id: raise HTTPException( @@ -142,7 +189,7 @@ async def list_companies( return { "items": [ - CompanyResponseDTO.model_validate(service.flatten_company_dto(item)) + CompanyResponseDTO.model_validate(service.flatten_company_dto(item)) for item in items ], "total": total, @@ -167,6 +214,8 @@ async def get_my_companies( Un usuario solo con roles de app y sin ``tenant_id`` en /auth/me sigue pudiendo listar sus compañías asignadas. """ + _assert_permission_any_company(db, current_user, "cat_company.view") + user_id = current_user.get("sub") or current_user.get("id") tenant_id = resolve_effective_tenant_id_from_user(current_user) @@ -194,6 +243,8 @@ async def get_company( current_user: dict = Depends(get_current_user), ): """Get a specific company by ID""" + _assert_permission_for_company(db, company_id, current_user, "cat_company.view") + tenant_id = current_user.get("tenant_id") if not tenant_id: raise HTTPException( @@ -224,6 +275,8 @@ async def update_company( current_user: dict = Depends(get_current_user), ): """Update a company""" + _assert_permission_for_company(db, company_id, current_user, "cat_company.edit") + tenant_id = current_user.get("tenant_id") if not tenant_id: raise HTTPException( @@ -299,6 +352,8 @@ async def delete_company( current_user: dict = Depends(get_current_user), ): """Delete a company""" + _assert_permission_for_company(db, company_id, current_user, "cat_company.delete") + tenant_id = current_user.get("tenant_id") if not tenant_id: raise HTTPException( @@ -328,6 +383,8 @@ async def upload_company_logo( current_user: dict = Depends(get_current_user), ): """Upload a logo for a company""" + _assert_permission_for_company(db, company_id, current_user, "cat_company.edit") + tenant_id = _resolve_tenant_id_int(current_user) # Validar que la empresa existe @@ -409,6 +466,8 @@ async def upload_company_certificate( Upload a certificate for a company certificate_type: fiel_cer, fiel_key, cfdi_cert_cer, cfdi_cert_key, cancel_cer, cancel_key """ + _assert_permission_for_company(db, company_id, current_user, "cat_company.edit") + tenant_id = _resolve_tenant_id_int(current_user) # Validar que la empresa existe diff --git a/backend/api/v1/modules/a76/general_catalogs/depreciation_catalog/routes.py b/backend/api/v1/modules/a76/general_catalogs/depreciation_catalog/routes.py index 20f2c9a9..56f5086a 100644 --- a/backend/api/v1/modules/a76/general_catalogs/depreciation_catalog/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/depreciation_catalog/routes.py @@ -30,6 +30,10 @@ base_router = TenantCRUDRoutes( enable_filters=False, default_page_size=100, max_page_size=1000, + get_permissions=["cat_depreciation_catalog.view"], + create_permissions=["cat_depreciation_catalog.create"], + update_permissions=["cat_depreciation_catalog.edit"], + delete_permissions=["cat_depreciation_catalog.delete"], ) router = APIRouter(prefix="/depreciation-catalog", tags=["a76 / general catalogs / depreciation catalog"]) @@ -49,7 +53,9 @@ async def list_depreciation_catalog( 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_depreciation_catalog.view"] + ) items, total = DepreciationCatalogService.get_all( db, tenant_id, company_id, page, page_size, search diff --git a/backend/api/v1/modules/a76/general_catalogs/doda/routes.py b/backend/api/v1/modules/a76/general_catalogs/doda/routes.py index 05b18223..bec0c7c5 100644 --- a/backend/api/v1/modules/a76/general_catalogs/doda/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/doda/routes.py @@ -254,7 +254,9 @@ async def print_doda_pdf( Genera o reutiliza el PDF almacenado en S3 cuando el contenido no ha cambiado (huella SHA-256 de DODA + hijos). """ - tenant_id = int(validate_access_to_resource(db, company_id, current_user)) + tenant_id = int( + validate_access_to_resource(db, company_id, current_user, ["cat_doda.view"]) + ) doda = DodaService.get_by_id(db, doda_id, tenant_id, company_id) if not doda: raise HTTPException( @@ -327,9 +329,12 @@ async def print_doda_pdf( ) async def get_doda_containers( doda_id: int, + company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), ): """Get all containers for a specific DODA""" + validate_access_to_resource(db, company_id, current_user, ["cat_doda.view"]) containers = DodaService.get_containers(db, doda_id) return [DodaContainerResponseDTO.model_validate(c) for c in containers] @@ -343,9 +348,12 @@ async def get_doda_containers( async def add_container( doda_id: int, container_data: DodaContainerCreateDTO, + company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), ): """Add a new container to a DODA""" + validate_access_to_resource(db, company_id, current_user, ["cat_doda.edit"]) container = DodaService.add_container(db, doda_id, container_data) if not container: raise HTTPException( @@ -364,9 +372,12 @@ async def update_container( doda_id: int, container_line: int, container_data: DodaContainerUpdateDTO, + company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), ): """Update a container""" + validate_access_to_resource(db, company_id, current_user, ["cat_doda.edit"]) container = DodaService.update_container( db, doda_id, container_line, container_data ) @@ -386,12 +397,15 @@ async def update_container( async def delete_container( doda_id: int, container_line: int, + company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), ): """ Elimina un contenedor del DODA. Devuelve 409 si el contenedor tiene precintos (candados) asignados. """ + validate_access_to_resource(db, company_id, current_user, ["cat_doda.delete"]) DodaService.delete_container(db, doda_id, container_line) return None @@ -415,8 +429,11 @@ def _seal_to_response(seal) -> DodaContainerSealResponseDTO: async def get_container_seals( doda_id: int, container_line: int, + company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), ): + validate_access_to_resource(db, company_id, current_user, ["cat_doda.view"]) seals = DodaService.get_seals_for_container(db, doda_id, container_line) return [_seal_to_response(s) for s in seals] @@ -431,8 +448,11 @@ async def add_container_seal( doda_id: int, container_line: int, seal_data: DodaContainerSealCreateDTO, + company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), ): + validate_access_to_resource(db, company_id, current_user, ["cat_doda.edit"]) seal = DodaService.add_seal(db, doda_id, container_line, seal_data) return _seal_to_response(seal) @@ -446,8 +466,11 @@ async def delete_container_seal( doda_id: int, container_line: int, seal_line: int, + company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), ): + validate_access_to_resource(db, company_id, current_user, ["cat_doda.delete"]) DodaService.delete_seal(db, doda_id, container_line, seal_line) return None @@ -460,9 +483,12 @@ async def delete_container_seal( ) async def get_american_pedimentos( doda_id: int, + company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), ): """Get all American pedimentos for a specific DODA""" + validate_access_to_resource(db, company_id, current_user, ["cat_doda.view"]) pedimentos = DodaService.get_american_pedimentos(db, doda_id) return [DodaAmericanPedimentoResponseDTO.model_validate(p) for p in pedimentos] @@ -476,9 +502,12 @@ async def get_american_pedimentos( async def add_american_pedimento( doda_id: int, pedimento_data: DodaAmericanPedimentoCreateDTO, + company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), ): """Add a new American pedimento to a DODA""" + validate_access_to_resource(db, company_id, current_user, ["cat_doda.edit"]) pedimento = DodaService.add_american_pedimento(db, doda_id, pedimento_data) if not pedimento: raise HTTPException( @@ -497,9 +526,12 @@ async def add_american_pedimento( async def delete_american_pedimento( doda_id: int, pedimento_line: int, + company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), ): """Elimina un pedimento americano del DODA.""" + validate_access_to_resource(db, company_id, current_user, ["cat_doda.delete"]) DodaService.delete_american_pedimento(db, doda_id, pedimento_line) return None @@ -512,9 +544,12 @@ async def delete_american_pedimento( ) async def get_doda_pedimentos( doda_id: int, + company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), ): """Get all pedimentos for a specific DODA""" + validate_access_to_resource(db, company_id, current_user, ["cat_doda.view"]) pedimentos = DodaService.get_pedimentos(db, doda_id) return [DodaPedimentoResponseDTO.model_validate(p) for p in pedimentos] @@ -528,9 +563,12 @@ async def get_doda_pedimentos( async def add_pedimento( doda_id: int, pedimento_data: DodaPedimentoCreateDTO, + company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), ): """Add a new pedimento to a DODA""" + validate_access_to_resource(db, company_id, current_user, ["cat_doda.edit"]) pedimento = DodaService.add_pedimento(db, doda_id, pedimento_data) if not pedimento: raise HTTPException( @@ -548,9 +586,12 @@ async def add_pedimento( async def delete_pedimento( doda_id: int, pedimento_line: int, + company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), ): """Elimina un pedimento del DODA.""" + validate_access_to_resource(db, company_id, current_user, ["cat_doda.delete"]) DodaService.delete_pedimento(db, doda_id, pedimento_line) return None @@ -575,7 +616,7 @@ async def get_doda_elegibilidad( Porta las validaciones del sistema legacy (campos requeridos, max 4 contenedores, gafete si DODA, patente vs agente, certificados DODA en VU). """ - tenant_id = validate_access_to_resource(db, company_id, current_user) + tenant_id = validate_access_to_resource(db, company_id, current_user, ["cat_doda.view"]) user_email = ( current_user.get("email") or current_user.get("preferred_username") @@ -614,7 +655,7 @@ async def post_doda_alta( Verifica elegibilidad, construye el payload desde los datos del DODA y su VU, y envía el alta al servicio externo. Retorna {task_id, status, message} para polling. """ - tenant_id = validate_access_to_resource(db, company_id, current_user) + tenant_id = validate_access_to_resource(db, company_id, current_user, ["cat_doda.edit"]) user_email = ( current_user.get("email") or current_user.get("preferred_username") @@ -694,11 +735,14 @@ async def post_doda_alta( ) async def get_doda_alta_status( task_id: str, + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user), ) -> Any: """ Proxy transparente al servicio externo para consultar el estado de una tarea de alta DODA. """ + validate_access_to_resource(db, company_id, current_user, ["cat_doda.view"]) try: ext = DodaExternalService() return ext.get_status(task_id) @@ -727,7 +771,7 @@ async def post_doda_consulta( db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user), ) -> Dict[str, Any]: - tenant_id = validate_access_to_resource(db, company_id, current_user) + tenant_id = validate_access_to_resource(db, company_id, current_user, ["cat_doda.edit"]) user_email = ( current_user.get("email") or current_user.get("preferred_username") @@ -788,8 +832,11 @@ async def post_doda_consulta( ) async def get_doda_consulta_status( task_id: str, + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user), ) -> Any: + validate_access_to_resource(db, company_id, current_user, ["cat_doda.view"]) try: ext = DodaExternalService() return ext.get_consulta_status(task_id) @@ -818,7 +865,9 @@ async def post_doda_consulta_apply( db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user), ) -> Dict[str, Any]: - tenant_id = int(validate_access_to_resource(db, company_id, current_user)) + tenant_id = int( + validate_access_to_resource(db, company_id, current_user, ["cat_doda.edit"]) + ) doda_record = DodaService.get_by_id(db, doda_id, tenant_id, company_id) if not doda_record: raise HTTPException(status_code=404, detail="DODA no encontrado.") @@ -880,7 +929,7 @@ async def post_doda_eliminar( db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user), ) -> Dict[str, Any]: - tenant_id = validate_access_to_resource(db, company_id, current_user) + tenant_id = validate_access_to_resource(db, company_id, current_user, ["cat_doda.delete"]) user_email = ( current_user.get("email") or current_user.get("preferred_username") @@ -960,8 +1009,11 @@ async def post_doda_eliminar( ) async def get_doda_eliminar_status( task_id: str, + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user), ) -> Any: + validate_access_to_resource(db, company_id, current_user, ["cat_doda.view"]) try: ext = DodaExternalService() return ext.get_eliminar_status(task_id) @@ -996,7 +1048,7 @@ async def list_doda_alta_logs( 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, ["cat_doda.view"]) return DodaAltaLogService.list( db, company_id, int(tenant_id), page, page_size, doda_id, search ) @@ -1014,7 +1066,7 @@ async def get_doda_alta_log( 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, ["cat_doda.view"]) record = DodaAltaLogService.get(db, log_id, company_id, int(tenant_id)) if not record: raise HTTPException(status_code=404, detail="Registro de alta DODA no encontrado.") @@ -1034,7 +1086,7 @@ async def create_doda_alta_log( 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, ["cat_doda.create"]) record = DodaAltaLogService.create(db, dto, company_id, int(tenant_id)) return DodaAltaLogResponseDTO.model_validate(record) @@ -1052,7 +1104,7 @@ async def update_doda_alta_log( 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, ["cat_doda.edit"]) record = DodaAltaLogService.get(db, log_id, company_id, int(tenant_id)) if not record: raise HTTPException(status_code=404, detail="Registro de alta DODA no encontrado.") @@ -1072,7 +1124,7 @@ async def delete_doda_alta_log( 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, ["cat_doda.delete"]) record = DodaAltaLogService.get(db, log_id, company_id, int(tenant_id)) if not record: raise HTTPException(status_code=404, detail="Registro de alta DODA no encontrado.") diff --git a/backend/api/v1/modules/a76/general_catalogs/error_catalogs/routes.py b/backend/api/v1/modules/a76/general_catalogs/error_catalogs/routes.py index 2feb6f7e..6af9fea3 100644 --- a/backend/api/v1/modules/a76/general_catalogs/error_catalogs/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/error_catalogs/routes.py @@ -4,12 +4,12 @@ Rutas para gestión de catálogos de errores from typing import List -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.orm import Session from core.database import get_core_db from core.security import get_current_user -from api.v1.common.tenant_crud_routes import TenantCRUDRoutes +from api.v1.common.tenant_crud_routes import TenantCRUDRoutes, validate_access_to_resource from .dto import ( ErrorClassificationCreateDTO, ErrorClassificationResponseDTO, @@ -35,6 +35,11 @@ classification_crud = TenantCRUDRoutes( tags=["error-classifications"], resource_name="ErrorClassification", enable_list=True, + list_permissions=["cat_errors.view"], + get_permissions=["cat_errors.view"], + create_permissions=["cat_errors.create"], + update_permissions=["cat_errors.edit"], + delete_permissions=["cat_errors.delete"], ) # Add custom endpoints for classifications @@ -47,15 +52,17 @@ classification_crud = TenantCRUDRoutes( ) async def get_classification_by_code( code: str, + company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user), ): """Get an error classification by its code with all related errors""" + tenant_id = validate_access_to_resource(db, company_id, current_user, ["cat_errors.view"]) classification = ErrorClassificationService.get_by_code( db, code, - tenant_id=current_user["tenant_id"], - company_id=current_user["company_id"] + tenant_id=tenant_id, + company_id=company_id, ) if not classification: raise HTTPException( @@ -74,15 +81,17 @@ async def get_classification_by_code( ) async def get_classification( id: int, + company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user), ): """Get an error classification by its ID with all related errors""" + tenant_id = validate_access_to_resource(db, company_id, current_user, ["cat_errors.view"]) classification = ErrorClassificationService.get_by_id( db, id, - tenant_id=current_user["tenant_id"], - company_id=current_user["company_id"] + tenant_id=tenant_id, + company_id=company_id, ) if not classification: raise HTTPException( @@ -104,6 +113,11 @@ catalog_crud = TenantCRUDRoutes( tags=["error-catalogs"], resource_name="ErrorCatalog", enable_list=True, + list_permissions=["cat_errors.view"], + get_permissions=["cat_errors.view"], + create_permissions=["cat_errors.create"], + update_permissions=["cat_errors.edit"], + delete_permissions=["cat_errors.delete"], ) # Add custom endpoints for catalogs @@ -116,15 +130,17 @@ catalog_crud = TenantCRUDRoutes( ) async def get_error_by_code( code: str, + company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user), ): """Get an error by its code with classification details""" + tenant_id = validate_access_to_resource(db, company_id, current_user, ["cat_errors.view"]) error = ErrorCatalogService.get_by_code( db, code, - tenant_id=current_user["tenant_id"], - company_id=current_user["company_id"] + tenant_id=tenant_id, + company_id=company_id, ) if not error: raise HTTPException( @@ -141,15 +157,17 @@ async def get_error_by_code( ) async def get_errors_by_classification( classification_id: int, + company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user), ): """Get all errors for a specific classification""" + tenant_id = validate_access_to_resource(db, company_id, current_user, ["cat_errors.view"]) errors = ErrorCatalogService.get_by_classification( db, classification_id, - tenant_id=current_user["tenant_id"], - company_id=current_user["company_id"] + tenant_id=tenant_id, + company_id=company_id, ) return [ErrorCatalogResponseDTO.model_validate(error) for error in errors] @@ -163,15 +181,17 @@ async def get_errors_by_classification( ) async def get_error( id: int, + company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user), ): """Get an error by its ID with classification details""" + tenant_id = validate_access_to_resource(db, company_id, current_user, ["cat_errors.view"]) error = ErrorCatalogService.get_by_id( db, id, - tenant_id=current_user["tenant_id"], - company_id=current_user["company_id"] + tenant_id=tenant_id, + company_id=company_id, ) if not error: raise HTTPException( diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/canadian_tariff_fractions/routes.py b/backend/api/v1/modules/a76/general_catalogs/fractions/canadian_tariff_fractions/routes.py index ae872c3b..757e0bec 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/canadian_tariff_fractions/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/canadian_tariff_fractions/routes.py @@ -25,9 +25,9 @@ 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) + tenant_id = validate_access_to_resource( + db, company_id, current_user, ["frac_canadian.view"] + ) skip = (page - 1) * page_size service = CanadianTariffFractionService(db) items, total = service.get_multi( @@ -52,9 +52,9 @@ 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) + tenant_id = validate_access_to_resource( + db, company_id, current_user, ["frac_canadian.view"] + ) service = CanadianTariffFractionService(db) item = service.get(id, tenant_id, company_id) if not item: @@ -68,9 +68,9 @@ 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) + tenant_id = validate_access_to_resource( + db, company_id, current_user, ["frac_canadian.create"] + ) service = CanadianTariffFractionService(db) return service.create(item_in, tenant_id, company_id) @@ -82,9 +82,9 @@ 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) + tenant_id = validate_access_to_resource( + db, company_id, current_user, ["frac_canadian.edit"] + ) service = CanadianTariffFractionService(db) item = service.get(id, tenant_id, company_id) if not item: @@ -98,9 +98,9 @@ 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) + tenant_id = validate_access_to_resource( + db, company_id, current_user, ["frac_canadian.delete"] + ) service = CanadianTariffFractionService(db) item = service.get(id, tenant_id, company_id) if not item: diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/historical_tariff_fractions/routes.py b/backend/api/v1/modules/a76/general_catalogs/fractions/historical_tariff_fractions/routes.py index 648b98a2..5dca0eb2 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/historical_tariff_fractions/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/historical_tariff_fractions/routes.py @@ -20,9 +20,9 @@ def get_historical_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_historical.view"]) - tenant_id = validate_access_to_resource(db, company_id, current_user) + tenant_id = validate_access_to_resource( + db, company_id, current_user, ["frac_historical.view"] + ) skip = (page - 1) * page_size service = HistoricalTariffFractionService(db) items, total = service.get_multi(tenant_id, company_id, skip=skip, limit=page_size, historical_fraction=historical_fraction) @@ -46,9 +46,9 @@ async def get_rate( 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_historical.view"]) - tenant_id = validate_access_to_resource(db, company_id, current_user) + tenant_id = validate_access_to_resource( + db, company_id, current_user, ["frac_historical.view"] + ) # Parse date try: @@ -80,9 +80,9 @@ def get_historical_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_historical.view"]) - tenant_id = validate_access_to_resource(db, company_id, current_user) + tenant_id = validate_access_to_resource( + db, company_id, current_user, ["frac_historical.view"] + ) service = HistoricalTariffFractionService(db) fraction = service.get(id, tenant_id, company_id) if not fraction: @@ -96,9 +96,9 @@ def create_historical_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_historical.create"]) - tenant_id = validate_access_to_resource(db, company_id, current_user) + tenant_id = validate_access_to_resource( + db, company_id, current_user, ["frac_historical.create"] + ) service = HistoricalTariffFractionService(db) return service.create(fraction_in, tenant_id, company_id) @@ -110,9 +110,9 @@ def update_historical_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_historical.edit"]) - tenant_id = validate_access_to_resource(db, company_id, current_user) + tenant_id = validate_access_to_resource( + db, company_id, current_user, ["frac_historical.edit"] + ) service = HistoricalTariffFractionService(db) fraction = service.get(id, tenant_id, company_id) if not fraction: @@ -126,9 +126,9 @@ def delete_historical_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_historical.delete"]) - tenant_id = validate_access_to_resource(db, company_id, current_user) + tenant_id = validate_access_to_resource( + db, company_id, current_user, ["frac_historical.delete"] + ) service = HistoricalTariffFractionService(db) fraction = service.get(id, tenant_id, company_id) if not fraction: diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/routes.py b/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/routes.py index 006250d0..52c5e05b 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/routes.py @@ -40,8 +40,10 @@ async def list_tariff_fractions( current_user: Dict[str, Any] = Depends(get_current_user), ): # Validar permisos según el catálogo solicitado - if catalog in ["mex", "usa"]: + if catalog == "mex": validate_access_to_resource(db, company_id, current_user, ["frac_sitar.view"]) + elif catalog == "usa": + validate_access_to_resource(db, company_id, current_user, ["frac_sitar_us.view"]) elif catalog == "american": validate_access_to_resource(db, company_id, current_user, ["frac_american.view"]) @@ -83,12 +85,19 @@ async def list_tariff_fractions( ) async def get_tariff_fraction( tariff_fraction_id: int, + catalog: str = Query("mex", description="Catalog source: 'mex', 'usa', or 'american'"), 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"]) + if catalog == "mex": + validate_access_to_resource(db, company_id, current_user, ["frac_sitar.view"]) + elif catalog == "usa": + validate_access_to_resource(db, company_id, current_user, ["frac_sitar_us.view"]) + elif catalog == "american": + validate_access_to_resource(db, company_id, current_user, ["frac_american.view"]) + else: + raise HTTPException(status_code=400, detail=f"Unsupported catalog '{catalog}'") item = TariffFractionService.get_by_id(db, tariff_fraction_id) if not item: from fastapi import HTTPException diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py index a1cee9f5..acda6661 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py @@ -77,11 +77,9 @@ async def list_us_tariff_fractions( db: Session = Depends(get_core_db), current_user: Dict[str, Any] = Depends(get_current_user), ): - from core.security import validate_access_to_resource as validate_perm - validate_perm(db, company_id, current_user, ["frac_american.view"]) - tenant_id = validate_access_to_resource(db, company_id, current_user) - - validate_access_to_resource(db, company_id, current_user) + tenant_id = validate_access_to_resource( + db, company_id, current_user, ["frac_sitar_us.view"] + ) skip = (page - 1) * page_size try: diff --git a/backend/api/v1/modules/a76/general_catalogs/location/routes.py b/backend/api/v1/modules/a76/general_catalogs/location/routes.py index bd9949a5..529ad1bf 100644 --- a/backend/api/v1/modules/a76/general_catalogs/location/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/location/routes.py @@ -17,4 +17,9 @@ router = TenantCRUDRoutes( enable_list=True, enable_filters=True, max_page_size=1000, + list_permissions=["cat_locations.view"], + get_permissions=["cat_locations.view"], + create_permissions=["cat_locations.create"], + update_permissions=["cat_locations.edit"], + delete_permissions=["cat_locations.delete"], ).router diff --git a/backend/api/v1/modules/a76/general_catalogs/prevalidators/routes.py b/backend/api/v1/modules/a76/general_catalogs/prevalidators/routes.py index 71ee0120..539a2d6e 100644 --- a/backend/api/v1/modules/a76/general_catalogs/prevalidators/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/prevalidators/routes.py @@ -4,12 +4,12 @@ Rutas para gestión de prevalidadores from typing import List -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.orm import Session from core.database import get_core_db from core.security import get_current_user -from api.v1.common.tenant_crud_routes import TenantCRUDRoutes +from api.v1.common.tenant_crud_routes import TenantCRUDRoutes, validate_access_to_resource from .dto import ( PrevalidatorCreateDTO, PrevalidatorResponseDTO, @@ -29,6 +29,11 @@ prevalidator_crud = TenantCRUDRoutes( tags=["Prevalidators"], resource_name="Prevalidator", enable_list=True, + list_permissions=["cat_prevalidators.view"], + get_permissions=["cat_prevalidators.view"], + create_permissions=["cat_prevalidators.create"], + update_permissions=["cat_prevalidators.edit"], + delete_permissions=["cat_prevalidators.delete"], ) # Add custom endpoints @@ -41,15 +46,19 @@ prevalidator_crud = TenantCRUDRoutes( ) async def get_prevalidator_by_code( code: str, + company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user), ): """Get a prevalidator by its code""" + tenant_id = validate_access_to_resource( + db, company_id, current_user, ["cat_prevalidators.view"] + ) prevalidator = PrevalidatorService.get_by_code( db, code, - tenant_id=current_user["tenant_id"], - company_id=current_user["company_id"] + tenant_id=tenant_id, + company_id=company_id, ) if not prevalidator: raise HTTPException( @@ -78,9 +87,12 @@ async def create_prevalidator( async def update_prevalidator( prevalidator_id: int, prevalidator_data: PrevalidatorUpdateDTO, + company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), ): """Update a prevalidator""" + validate_access_to_resource(db, company_id, current_user, ["cat_prevalidators.edit"]) prevalidator = PrevalidatorService.update( db, prevalidator_id, prevalidator_data) if not prevalidator: @@ -98,9 +110,12 @@ async def update_prevalidator( ) async def delete_prevalidator( prevalidator_id: int, + company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), ): """Delete a prevalidator""" + validate_access_to_resource(db, company_id, current_user, ["cat_prevalidators.delete"]) success = PrevalidatorService.delete(db, prevalidator_id) if not success: raise HTTPException( @@ -117,10 +132,15 @@ async def delete_prevalidator( ) async def get_prevalidators_by_customs( customs: str, + company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), ): """Get all prevalidators for a specific customs""" - prevalidators = PrevalidatorService.get_by_customs(db, customs) + tenant_id = validate_access_to_resource( + db, company_id, current_user, ["cat_prevalidators.view"] + ) + prevalidators = PrevalidatorService.get_by_customs(db, customs, tenant_id, company_id) return [ PrevalidatorResponseDTO.model_validate(prevalidator) for prevalidator in prevalidators diff --git a/backend/api/v1/modules/a76/general_catalogs/signatures/routes.py b/backend/api/v1/modules/a76/general_catalogs/signatures/routes.py index f7c390a5..0e228d57 100644 --- a/backend/api/v1/modules/a76/general_catalogs/signatures/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/signatures/routes.py @@ -27,6 +27,11 @@ signature_crud = TenantCRUDRoutes( enable_filters=True, # Enable filtering by code default_page_size=50, max_page_size=100, + list_permissions=["cat_signatures.view"], + get_permissions=["cat_signatures.view"], + create_permissions=["cat_signatures.create"], + update_permissions=["cat_signatures.edit"], + delete_permissions=["cat_signatures.delete"], ) router = signature_crud.router @@ -44,7 +49,9 @@ async def get_signature_by_code( current_user: Dict[str, Any] = Depends(signature_crud.auth_dependency), ): """Get a signature by its code""" - tenant_id = validate_access_to_resource(db, company_id, current_user) + tenant_id = validate_access_to_resource( + db, company_id, current_user, ["cat_signatures.view"] + ) signature = SignatureService.get_by_code(db, code, tenant_id, company_id) if not signature: raise HTTPException( @@ -61,9 +68,12 @@ async def get_signature_by_code( ) async def delete_signature( signature_id: int, + company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(signature_crud.auth_dependency), ): """Delete a signature""" + validate_access_to_resource(db, company_id, current_user, ["cat_signatures.delete"]) success = SignatureService.delete(db, signature_id) if not success: raise HTTPException( diff --git a/backend/api/v1/modules/core/permissions/seed_v2.py b/backend/api/v1/modules/core/permissions/seed_v2.py index cda18719..00e92e10 100644 --- a/backend/api/v1/modules/core/permissions/seed_v2.py +++ b/backend/api/v1/modules/core/permissions/seed_v2.py @@ -77,12 +77,9 @@ permissions_general = ( gen_crud("cat_broker_concepts", "Conceptos de Agente Aduanal", "general_catalogs") + gen_crud("cat_classification", "Clasificación de Conceptos", "general_catalogs") + gen_crud("cat_identifiers", "Identificadores", "general_catalogs") + - gen_crud("cat_incoterms", "Incoterms (Catálogo)", "general_catalogs") + gen_crud("cat_inpc", "INPC", "general_catalogs") + gen_crud("cat_legends", "Leyendas Fijas", "general_catalogs") + gen_crud("cat_seals", "Sellos", "general_catalogs") + - gen_crud("cat_valuation", "Métodos de Valoración", "general_catalogs") + - gen_crud("cat_countries", "Países", "general_catalogs") + gen_crud("cat_ports", "Puertos", "general_catalogs") + gen_crud("cat_um_general", "UdM Generales", "general_catalogs") + gen_crud("cat_um_customs", "UdM Aduana MEX", "general_catalogs") + @@ -92,17 +89,15 @@ permissions_general = ( gen_crud("cat_unit_conversions", "Conversiones", "general_catalogs") + gen_crud("cat_equivalencies", "Equivalencias", "general_catalogs") + gen_crud("cat_exchange_rates", "Tipos de Cambio", "general_catalogs") + - gen_crud("cat_currency", "Tipos de Moneda", "general_catalogs") + gen_crud("cat_multi_currency_types", "Tipos de Moneda (Múltiples)", "general_catalogs") + - gen_crud("cat_inv_types", "Tipos de Factura", "general_catalogs") + gen_crud("cat_signatures", "Firmas Electrónicas", "general_catalogs") + gen_crud("cat_errors", "Catálogo de Errores", "general_catalogs") + gen_crud("cat_doda", "DODA", "general_catalogs") + gen_crud("cat_prevalidators", "Prevalidadores", "general_catalogs") + gen_crud("cat_notices", "Avisos Electrónicos", "general_catalogs") + - gen_crud("cat_crossing", "Avisos de Cruce", "general_catalogs") + - gen_crud("cat_warehouses", "Recintos", "general_catalogs") + - gen_crud("cat_sectors", "Sectores", "general_catalogs") + gen_crud("cat_sectors", "Sectores", "general_catalogs") + + gen_crud("cat_locations", "Ubicaciones", "general_catalogs") + + gen_crud("cat_depreciation_catalog", "Catálogo de Depreciación", "general_catalogs") ) # ============================================================================ diff --git a/frontend/src/lib/components/dashboard/general_catalogs/doda/doda-form-modal.svelte b/frontend/src/lib/components/dashboard/general_catalogs/doda/doda-form-modal.svelte index 519a708f..1e6b5073 100644 --- a/frontend/src/lib/components/dashboard/general_catalogs/doda/doda-form-modal.svelte +++ b/frontend/src/lib/components/dashboard/general_catalogs/doda/doda-form-modal.svelte @@ -70,12 +70,14 @@ let { dodaIdParam, defaultLastUser: defaultLastUserProp = '', + canMutate = true, onClose, onCreatedNavigateTo }: { /** 'new' o id numérico en string; desde ?doda_id= de la URL */ dodaIdParam: string; defaultLastUser?: string; + canMutate?: boolean; onClose: () => void; /** Tras crear DODA, actualiza ?doda_id= al id creado (sin desmontar la lista) */ onCreatedNavigateTo: (dodaId: number) => void; @@ -170,13 +172,22 @@ dodaFormT(dodaLoc, 'shortcuts_scope'), obtenerAtajosFormularioDodaPagina({ cambiarPestana: (pestana) => (activeTab = pestana), - manejarGuardar: handleSubmit, + manejarGuardar: () => { + if (!canMutate) return; + handleSubmit(); + }, manejarCerrar: () => { if (browser) onClose(); } }) ); + function ensureCanMutate(): boolean { + if (canMutate) return true; + error = 'Permission denied: cat_doda.create/edit'; + return false; + } + function getEmptyForm(): DodaCreate & { pedimentos_detail?: any[]; containers?: any[]; @@ -400,6 +411,7 @@ } async function handleAddSeal() { + if (!ensureCanMutate()) return; error = null; if (!isEdit || !id) { error = t('seal_save_first'); @@ -457,6 +469,7 @@ } async function handleAddSealFromContainerModal() { + if (!ensureCanMutate()) return; const trimmed = newSealValue.trim(); if (!trimmed) { error = t('seal_empty'); @@ -493,6 +506,7 @@ } function handleAmericanPedimentoModalConfirm() { + if (!ensureCanMutate()) return; error = null; const tipo = americanModalType.trim(); const valor = americanModalValue.trim(); @@ -539,6 +553,7 @@ } async function handleDeleteSeal(sealLine: number) { + if (!ensureCanMutate()) return; error = null; if (!isEdit || !id) return; if (selectedContainerIndex == null) return; @@ -563,6 +578,7 @@ } async function handleSubmit() { + if (!ensureCanMutate()) return; submitAttempted = true; error = null; warning = null; @@ -1621,7 +1637,7 @@