From 5c1513eb6542176efcc2ce80d4b4b415dcc43f97 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Tue, 9 Jun 2026 12:56:28 +0800 Subject: [PATCH 1/8] feat(api_v2): split program endpoints into spp_api_v2_programs (#1081, #1082) Extract the Program and ProgramMembership REST API out of spp_api_v2 into a new spp_api_v2_programs companion (auto-installs when spp_api_v2 + spp_programs are both present), so spp_api_v2 no longer DIRECTLY depends on spp_programs. Moved into the companion: program / program_membership routers, services, schemas, filter configs, api.path records, the program scope-enforcement test, and the program filter routers (registered into the shared RESOURCE_SERVICES). Base spp_api_v2 drops the program imports, the spp_programs dependency, and the program data records. #1082: spp_api_v2_data has no program code of its own; it only pulled spp_programs transitively via spp_api_v2. The remaining transitive path (spp_api_v2 -> spp_source_tracking -> spp_programs) closes in #1084. --- spp_api_v2/__manifest__.py | 3 - spp_api_v2/data/api_path_data.xml | 20 - .../models/fastapi_endpoint_registry.py | 8 - spp_api_v2/routers/__init__.py | 2 - spp_api_v2/routers/filter.py | 61 +-- spp_api_v2/schemas/__init__.py | 2 - spp_api_v2/services/__init__.py | 2 - spp_api_v2/tests/__init__.py | 4 - spp_api_v2/tests/test_scope_enforcement.py | 95 ---- spp_api_v2_programs/README.rst | 113 +++++ spp_api_v2_programs/__init__.py | 6 + spp_api_v2_programs/__manifest__.py | 29 ++ spp_api_v2_programs/data/api_path_data.xml | 24 + .../data/filter_config_program.xml | 0 .../data/filter_config_program_membership.xml | 0 spp_api_v2_programs/models/__init__.py | 3 + .../models/fastapi_endpoint_programs.py | 37 ++ spp_api_v2_programs/pyproject.toml | 3 + spp_api_v2_programs/readme/DESCRIPTION.md | 21 + spp_api_v2_programs/readme/HISTORY.md | 3 + spp_api_v2_programs/routers/__init__.py | 5 + .../routers/program.py | 4 +- .../routers/program_filters.py | 80 ++++ .../routers/program_membership.py | 8 +- spp_api_v2_programs/schemas/__init__.py | 4 + .../schemas/program.py | 2 +- .../schemas/program_membership.py | 2 +- .../security/ir.model.access.csv | 1 + spp_api_v2_programs/services/__init__.py | 4 + .../services/program_membership_service.py | 0 .../services/program_service.py | 0 .../static/description/icon.png | Bin 0 -> 15480 bytes .../static/description/index.html | 450 ++++++++++++++++++ spp_api_v2_programs/tests/__init__.py | 7 + .../tests/test_program_api.py | 2 +- .../tests/test_program_membership_api.py | 2 +- .../tests/test_program_membership_service.py | 4 +- .../tests/test_program_service.py | 2 +- .../tests/test_scope_enforcement_program.py | 104 ++++ 39 files changed, 909 insertions(+), 208 deletions(-) create mode 100644 spp_api_v2_programs/README.rst create mode 100644 spp_api_v2_programs/__init__.py create mode 100644 spp_api_v2_programs/__manifest__.py create mode 100644 spp_api_v2_programs/data/api_path_data.xml rename {spp_api_v2 => spp_api_v2_programs}/data/filter_config_program.xml (100%) rename {spp_api_v2 => spp_api_v2_programs}/data/filter_config_program_membership.xml (100%) create mode 100644 spp_api_v2_programs/models/__init__.py create mode 100644 spp_api_v2_programs/models/fastapi_endpoint_programs.py create mode 100644 spp_api_v2_programs/pyproject.toml create mode 100644 spp_api_v2_programs/readme/DESCRIPTION.md create mode 100644 spp_api_v2_programs/readme/HISTORY.md create mode 100644 spp_api_v2_programs/routers/__init__.py rename {spp_api_v2 => spp_api_v2_programs}/routers/program.py (97%) create mode 100644 spp_api_v2_programs/routers/program_filters.py rename {spp_api_v2 => spp_api_v2_programs}/routers/program_membership.py (97%) create mode 100644 spp_api_v2_programs/schemas/__init__.py rename {spp_api_v2 => spp_api_v2_programs}/schemas/program.py (96%) rename {spp_api_v2 => spp_api_v2_programs}/schemas/program_membership.py (96%) create mode 100644 spp_api_v2_programs/security/ir.model.access.csv create mode 100644 spp_api_v2_programs/services/__init__.py rename {spp_api_v2 => spp_api_v2_programs}/services/program_membership_service.py (100%) rename {spp_api_v2 => spp_api_v2_programs}/services/program_service.py (100%) create mode 100644 spp_api_v2_programs/static/description/icon.png create mode 100644 spp_api_v2_programs/static/description/index.html create mode 100644 spp_api_v2_programs/tests/__init__.py rename {spp_api_v2 => spp_api_v2_programs}/tests/test_program_api.py (99%) rename {spp_api_v2 => spp_api_v2_programs}/tests/test_program_membership_api.py (99%) rename {spp_api_v2 => spp_api_v2_programs}/tests/test_program_membership_service.py (99%) rename {spp_api_v2 => spp_api_v2_programs}/tests/test_program_service.py (99%) create mode 100644 spp_api_v2_programs/tests/test_scope_enforcement_program.py diff --git a/spp_api_v2/__manifest__.py b/spp_api_v2/__manifest__.py index a6d9beb6a..cfbe15748 100644 --- a/spp_api_v2/__manifest__.py +++ b/spp_api_v2/__manifest__.py @@ -18,7 +18,6 @@ "spp_registry", "spp_consent", "spp_vocabulary", - "spp_programs", "spp_source_tracking", ], "data": [ @@ -30,8 +29,6 @@ "data/api_path_data.xml", "data/filter_config_individual.xml", "data/filter_config_group.xml", - "data/filter_config_program.xml", - "data/filter_config_program_membership.xml", "wizards/show_secret_wizard_views.xml", "views/api_client_views.xml", "views/api_extension_views.xml", diff --git a/spp_api_v2/data/api_path_data.xml b/spp_api_v2/data/api_path_data.xml index a71b86baf..0fef084d1 100644 --- a/spp_api_v2/data/api_path_data.xml +++ b/spp_api_v2/data/api_path_data.xml @@ -31,24 +31,4 @@ name="filter_domain" >[('is_registrant', '=', True), ('is_group', '=', True)] - - - - 30 - Program - - Social protection programs - False - 10 - - - - - 40 - ProgramMembership - - Beneficiary enrollments in programs - False - 10 - diff --git a/spp_api_v2/models/fastapi_endpoint_registry.py b/spp_api_v2/models/fastapi_endpoint_registry.py index dacbfb5ad..dd151c238 100644 --- a/spp_api_v2/models/fastapi_endpoint_registry.py +++ b/spp_api_v2/models/fastapi_endpoint_registry.py @@ -29,15 +29,11 @@ def _get_fastapi_routers(self) -> list[APIRouter]: from ..routers.filter import ( group_filter_router, individual_filter_router, - program_filter_router, - program_membership_filter_router, ) from ..routers.group import group_router from ..routers.individual import individual_router from ..routers.metadata import metadata_router from ..routers.oauth import oauth_router - from ..routers.program import program_router - from ..routers.program_membership import program_membership_router routers.extend( [ @@ -49,10 +45,6 @@ def _get_fastapi_routers(self) -> list[APIRouter]: group_filter_router, batch_router, bulk_router, - program_router, - program_filter_router, - program_membership_router, - program_membership_filter_router, consent_router, ] ) diff --git a/spp_api_v2/routers/__init__.py b/spp_api_v2/routers/__init__.py index 72ba7f58e..816920b47 100644 --- a/spp_api_v2/routers/__init__.py +++ b/spp_api_v2/routers/__init__.py @@ -6,5 +6,3 @@ from . import individual from . import metadata from . import oauth -from . import program -from . import program_membership diff --git a/spp_api_v2/routers/filter.py b/spp_api_v2/routers/filter.py index 7b00137cc..67c867e10 100644 --- a/spp_api_v2/routers/filter.py +++ b/spp_api_v2/routers/filter.py @@ -18,8 +18,6 @@ from ..services.filter_service import FilterService from ..services.group_service import GroupService from ..services.individual_service import IndividualService -from ..services.program_membership_service import ProgramMembershipService -from ..services.program_service import ProgramService _logger = logging.getLogger(__name__) @@ -27,11 +25,10 @@ # Router for filter metadata - mounted per resource individual_filter_router = APIRouter(tags=["Individual"], prefix="/Individual") group_filter_router = APIRouter(tags=["Group"], prefix="/Group") -program_filter_router = APIRouter(tags=["Program"], prefix="/Program") -program_membership_filter_router = APIRouter(tags=["ProgramMembership"], prefix="/ProgramMembership") -# Service mapping for resource types +# Service mapping for resource types. Companion modules (e.g. spp_api_v2_programs) +# register their own resources into this dict at import time. RESOURCE_SERVICES = { "Individual": { "service_class": IndividualService, @@ -45,18 +42,6 @@ "base_domain": [("is_registrant", "=", True), ("is_group", "=", True)], "consent_type": "group", }, - "Program": { - "service_class": ProgramService, - "model": "spp.program", - "base_domain": [], - "consent_type": None, - }, - "ProgramMembership": { - "service_class": ProgramMembershipService, - "model": "spp.program.membership", - "base_domain": [], - "consent_type": "program_membership", - }, } @@ -300,45 +285,3 @@ async def search( summary="Advanced Group Search", description="Search groups with complex filter conditions", ) - -# Register endpoints for Program -program_filter_router.add_api_route( - "/_filters", - _create_filter_metadata_endpoint("Program"), - methods=["GET"], - response_model=FilterMetadataResponse, - response_model_exclude_none=True, - summary="Get Program Filters", - description="Get available filters and presets for Program resource", -) - -program_filter_router.add_api_route( - "/_search", - _create_search_endpoint("Program"), - methods=["POST"], - response_model=Bundle, - response_model_exclude_none=True, - summary="Advanced Program Search", - description="Search programs with complex filter conditions", -) - -# Register endpoints for ProgramMembership -program_membership_filter_router.add_api_route( - "/_filters", - _create_filter_metadata_endpoint("ProgramMembership"), - methods=["GET"], - response_model=FilterMetadataResponse, - response_model_exclude_none=True, - summary="Get ProgramMembership Filters", - description="Get available filters and presets for ProgramMembership resource", -) - -program_membership_filter_router.add_api_route( - "/_search", - _create_search_endpoint("ProgramMembership"), - methods=["POST"], - response_model=Bundle, - response_model_exclude_none=True, - summary="Advanced ProgramMembership Search", - description="Search program memberships with complex filter conditions", -) diff --git a/spp_api_v2/schemas/__init__.py b/spp_api_v2/schemas/__init__.py index 6fd26c4a8..c85d70a75 100644 --- a/spp_api_v2/schemas/__init__.py +++ b/spp_api_v2/schemas/__init__.py @@ -9,6 +9,4 @@ from . import operation_outcome from . import patch from . import problem_detail -from . import program -from . import program_membership from . import search_result diff --git a/spp_api_v2/services/__init__.py b/spp_api_v2/services/__init__.py index 9b402a15b..1d0376a98 100644 --- a/spp_api_v2/services/__init__.py +++ b/spp_api_v2/services/__init__.py @@ -6,7 +6,5 @@ from . import filter_service from . import group_service from . import individual_service -from . import program_membership_service -from . import program_service from . import schema_builder from . import search_service diff --git a/spp_api_v2/tests/__init__.py b/spp_api_v2/tests/__init__.py index 4bfafb36e..806558639 100644 --- a/spp_api_v2/tests/__init__.py +++ b/spp_api_v2/tests/__init__.py @@ -31,10 +31,6 @@ from . import test_organization_type_security from . import test_pagination from . import test_patch_api -from . import test_program_api -from . import test_program_membership_api -from . import test_program_membership_service -from . import test_program_service from . import test_scope_enforcement from . import test_schema_builder from . import test_search_service diff --git a/spp_api_v2/tests/test_scope_enforcement.py b/spp_api_v2/tests/test_scope_enforcement.py index 8417bf966..df67b3122 100644 --- a/spp_api_v2/tests/test_scope_enforcement.py +++ b/spp_api_v2/tests/test_scope_enforcement.py @@ -487,101 +487,6 @@ def test_group_membership_history_requires_scope(self): self.assertIn("group:read", data["detail"]) -class TestScopeEnforcementProgramMembership(ApiV2HttpTestCase): - """Test scope enforcement on Program Membership endpoints""" - - def setUp(self): - super().setUp() - # Create test data - self.program = self.create_test_program(name="Test Program", target_type="individual") - self.individual = self.create_test_individual(identifier_value="SCOPE-PM-001") - - # Create test membership - self.membership = self.create_test_membership( - partner=self.individual, - program=self.program, - state="enrolled", - ) - - # Base URL for program membership - self.pm_url = "/api/v2/spp/ProgramMembership" - self.pm_id_url = f"{self.pm_url}/urn:openspp:vocab:id-type%23test_national_id|SCOPE-PM-001" - - def _make_client_without_scope(self, excluded_resource, excluded_action): - """Create a client that has all scopes EXCEPT the specified one""" - scopes = [] - # Give it a different resource scope to prove it's not resource-agnostic - other_resource = "individual" if excluded_resource == "program_membership" else "program_membership" - scopes.append({"resource": other_resource, "action": "all"}) - client = self.create_api_client( - name=f"No {excluded_resource}:{excluded_action}", - scopes=scopes, - require_consent=False, - ) - return client, self.generate_jwt_token(client) - - def test_program_membership_create_requires_scope(self): - """POST /ProgramMembership returns 403 without program_membership:create scope""" - client, token = self._make_client_without_scope("program_membership", "create") - - # Create new individual for enrollment - self.create_test_individual(identifier_value="SCOPE-PM-NEW") - - payload = { - "resourceType": "ProgramMembership", - "status": "enrolled", - "beneficiary": { - "reference": "Individual/urn:openspp:vocab:id-type#test_national_id|SCOPE-PM-NEW", - }, - "program": { - "reference": "Program/urn:openspp:program|test-program", - }, - } - - response = self.url_open( - self.pm_url, - data=json.dumps(payload), - headers={ - "Content-Type": "application/json", - "Authorization": f"Bearer {token}", - }, - ) - - self.assertEqual(response.status_code, 403) - data = json.loads(response.content) - self.assertIn("Missing required scope", data["detail"]) - self.assertIn("program_membership:create", data["detail"]) - - def test_program_membership_update_requires_scope(self): - """PUT /ProgramMembership/{id} returns 403 without program_membership:update scope""" - client, token = self._make_client_without_scope("program_membership", "update") - - payload = { - "resourceType": "ProgramMembership", - "status": "paused", - "beneficiary": { - "reference": "Individual/urn:openspp:vocab:id-type#test_national_id|SCOPE-PM-001", - }, - "program": { - "reference": "Program/urn:openspp:program|test-program", - }, - } - - response = self.url_put( - self.pm_id_url, - data=json.dumps(payload), - headers={ - "Content-Type": "application/json", - "Authorization": f"Bearer {token}", - }, - ) - - self.assertEqual(response.status_code, 403) - data = json.loads(response.content) - self.assertIn("Missing required scope", data["detail"]) - self.assertIn("program_membership:update", data["detail"]) - - class TestScopeIsolation(ApiV2HttpTestCase): """Test that scopes for one resource don't grant access to another""" diff --git a/spp_api_v2_programs/README.rst b/spp_api_v2_programs/README.rst new file mode 100644 index 000000000..c0a4f9cae --- /dev/null +++ b/spp_api_v2_programs/README.rst @@ -0,0 +1,113 @@ +========================= +OpenSPP API V2 - Programs +========================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:4471b988191c17dbafeaa6a0f0835db915192c85183aabb7f6a6bb66b8f0a213 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OpenSPP%2FOpenSPP2-lightgray.png?logo=github + :target: https://github.com/OpenSPP/OpenSPP2/tree/19.0/spp_api_v2_programs + :alt: OpenSPP/OpenSPP2 + +|badge1| |badge2| |badge3| + +Extends OpenSPP API V2 with REST endpoints for **Programs** and +**Program Memberships**. This module was split out of ``spp_api_v2`` so +that the base API module no longer depends on ``spp_programs`` — +deployments that don't use programs (e.g. a registry-only Social +Registry) can ship the API without installing the Programs stack. + +It auto-installs whenever both ``spp_api_v2`` and ``spp_programs`` are +present, so program API functionality is unchanged for deployments that +use programs. + +Key Capabilities +~~~~~~~~~~~~~~~~ + +- **Program endpoints**: read and search programs + (``/api/v2/spp/Program``) +- **Program Membership endpoints**: read, search, create, and update + beneficiary enrollments (``/api/v2/spp/ProgramMembership``) +- **Advanced filtering**: ``/_filters`` and ``/_search`` endpoints for + both resources, registered into the shared API V2 filter framework +- **OAuth scopes**: the ``program`` and ``program_membership`` + client-scope resources (defined in ``spp_api_v2``) gate access + +UI Location +~~~~~~~~~~~ + +No standalone menu. Endpoints are available under the API V2 app: + +- ``/api/v2/spp/Program``, ``/api/v2/spp/Program/{identifier}`` +- ``/api/v2/spp/ProgramMembership``, + ``/api/v2/spp/ProgramMembership/{identifier}`` + +Dependencies +~~~~~~~~~~~~ + +``spp_api_v2``, ``spp_programs`` + +**Table of contents** + +.. contents:: + :local: + +Changelog +========= + +19.0.1.0.0 +~~~~~~~~~~ + +- Split program and program-membership REST endpoints out of + ``spp_api_v2`` into this companion module (#1081), so ``spp_api_v2`` + no longer depends on ``spp_programs``. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* OpenSPP.org + +Maintainers +----------- + +.. |maintainer-jeremi| image:: https://github.com/jeremi.png?size=40px + :target: https://github.com/jeremi + :alt: jeremi +.. |maintainer-gonzalesedwin1123| image:: https://github.com/gonzalesedwin1123.png?size=40px + :target: https://github.com/gonzalesedwin1123 + :alt: gonzalesedwin1123 +.. |maintainer-emjay0921| image:: https://github.com/emjay0921.png?size=40px + :target: https://github.com/emjay0921 + :alt: emjay0921 + +Current maintainers: + +|maintainer-jeremi| |maintainer-gonzalesedwin1123| |maintainer-emjay0921| + +This module is part of the `OpenSPP/OpenSPP2 `_ project on GitHub. + +You are welcome to contribute. \ No newline at end of file diff --git a/spp_api_v2_programs/__init__.py b/spp_api_v2_programs/__init__.py new file mode 100644 index 000000000..57cf5d725 --- /dev/null +++ b/spp_api_v2_programs/__init__.py @@ -0,0 +1,6 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import models +from . import routers +from . import schemas +from . import services diff --git a/spp_api_v2_programs/__manifest__.py b/spp_api_v2_programs/__manifest__.py new file mode 100644 index 000000000..5becc04d0 --- /dev/null +++ b/spp_api_v2_programs/__manifest__.py @@ -0,0 +1,29 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +{ # pylint: disable=pointless-statement + "name": "OpenSPP API V2 - Programs", + "category": "OpenSPP/Integration", + "version": "19.0.1.0.0", + "sequence": 1, + "author": "OpenSPP.org", + "website": "https://github.com/OpenSPP/OpenSPP2", + "license": "LGPL-3", + "development_status": "Production/Stable", + "maintainers": ["jeremi", "gonzalesedwin1123", "emjay0921"], + "summary": "REST API endpoints for Programs and Program Memberships.", + "depends": [ + "spp_api_v2", + "spp_programs", + ], + "data": [ + "security/ir.model.access.csv", + "data/api_path_data.xml", + "data/filter_config_program.xml", + "data/filter_config_program_membership.xml", + ], + "assets": {}, + "demo": [], + "images": [], + "application": False, + "installable": True, + "auto_install": ["spp_api_v2", "spp_programs"], +} diff --git a/spp_api_v2_programs/data/api_path_data.xml b/spp_api_v2_programs/data/api_path_data.xml new file mode 100644 index 000000000..3acdd48e7 --- /dev/null +++ b/spp_api_v2_programs/data/api_path_data.xml @@ -0,0 +1,24 @@ + + + + + + + 30 + Program + + Social protection programs + False + 10 + + + + + 40 + ProgramMembership + + Beneficiary enrollments in programs + False + 10 + + diff --git a/spp_api_v2/data/filter_config_program.xml b/spp_api_v2_programs/data/filter_config_program.xml similarity index 100% rename from spp_api_v2/data/filter_config_program.xml rename to spp_api_v2_programs/data/filter_config_program.xml diff --git a/spp_api_v2/data/filter_config_program_membership.xml b/spp_api_v2_programs/data/filter_config_program_membership.xml similarity index 100% rename from spp_api_v2/data/filter_config_program_membership.xml rename to spp_api_v2_programs/data/filter_config_program_membership.xml diff --git a/spp_api_v2_programs/models/__init__.py b/spp_api_v2_programs/models/__init__.py new file mode 100644 index 000000000..371088cf6 --- /dev/null +++ b/spp_api_v2_programs/models/__init__.py @@ -0,0 +1,3 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import fastapi_endpoint_programs diff --git a/spp_api_v2_programs/models/fastapi_endpoint_programs.py b/spp_api_v2_programs/models/fastapi_endpoint_programs.py new file mode 100644 index 000000000..7f66c8f9e --- /dev/null +++ b/spp_api_v2_programs/models/fastapi_endpoint_programs.py @@ -0,0 +1,37 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Register the Program and ProgramMembership routers on the API V2 app.""" + +import logging + +from odoo import models + +from fastapi import APIRouter + +_logger = logging.getLogger(__name__) + + +class SppApiV2ProgramsEndpoint(models.Model): + """Extend the API V2 FastAPI endpoint with program routers.""" + + _inherit = "fastapi.endpoint" + + def _get_fastapi_routers(self) -> list[APIRouter]: + """Add Program / ProgramMembership routers to API V2.""" + routers = super()._get_fastapi_routers() + if self.app == "api_v2": + from ..routers.program import program_router + from ..routers.program_filters import ( + program_filter_router, + program_membership_filter_router, + ) + from ..routers.program_membership import program_membership_router + + routers.extend( + [ + program_router, + program_filter_router, + program_membership_router, + program_membership_filter_router, + ] + ) + return routers diff --git a/spp_api_v2_programs/pyproject.toml b/spp_api_v2_programs/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/spp_api_v2_programs/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/spp_api_v2_programs/readme/DESCRIPTION.md b/spp_api_v2_programs/readme/DESCRIPTION.md new file mode 100644 index 000000000..fa97a4bd5 --- /dev/null +++ b/spp_api_v2_programs/readme/DESCRIPTION.md @@ -0,0 +1,21 @@ +Extends OpenSPP API V2 with REST endpoints for **Programs** and **Program Memberships**. This module was split out of `spp_api_v2` so that the base API module no longer depends on `spp_programs` — deployments that don't use programs (e.g. a registry-only Social Registry) can ship the API without installing the Programs stack. + +It auto-installs whenever both `spp_api_v2` and `spp_programs` are present, so program API functionality is unchanged for deployments that use programs. + +### Key Capabilities + +- **Program endpoints**: read and search programs (`/api/v2/spp/Program`) +- **Program Membership endpoints**: read, search, create, and update beneficiary enrollments (`/api/v2/spp/ProgramMembership`) +- **Advanced filtering**: `/_filters` and `/_search` endpoints for both resources, registered into the shared API V2 filter framework +- **OAuth scopes**: the `program` and `program_membership` client-scope resources (defined in `spp_api_v2`) gate access + +### UI Location + +No standalone menu. Endpoints are available under the API V2 app: + +- `/api/v2/spp/Program`, `/api/v2/spp/Program/{identifier}` +- `/api/v2/spp/ProgramMembership`, `/api/v2/spp/ProgramMembership/{identifier}` + +### Dependencies + +`spp_api_v2`, `spp_programs` diff --git a/spp_api_v2_programs/readme/HISTORY.md b/spp_api_v2_programs/readme/HISTORY.md new file mode 100644 index 000000000..a87d1ffe4 --- /dev/null +++ b/spp_api_v2_programs/readme/HISTORY.md @@ -0,0 +1,3 @@ +### 19.0.1.0.0 + +- Split program and program-membership REST endpoints out of `spp_api_v2` into this companion module (#1081), so `spp_api_v2` no longer depends on `spp_programs`. diff --git a/spp_api_v2_programs/routers/__init__.py b/spp_api_v2_programs/routers/__init__.py new file mode 100644 index 000000000..d250089cb --- /dev/null +++ b/spp_api_v2_programs/routers/__init__.py @@ -0,0 +1,5 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import program +from . import program_filters +from . import program_membership diff --git a/spp_api_v2/routers/program.py b/spp_api_v2_programs/routers/program.py similarity index 97% rename from spp_api_v2/routers/program.py rename to spp_api_v2_programs/routers/program.py index c397ff2a2..e7405e9ef 100644 --- a/spp_api_v2/routers/program.py +++ b/spp_api_v2_programs/routers/program.py @@ -20,9 +20,9 @@ status, ) -from ..middleware.auth import get_authenticated_client +from odoo.addons.spp_api_v2.middleware.auth import get_authenticated_client from ..schemas.program import Program -from ..schemas.search_result import SearchResult, create_search_result +from odoo.addons.spp_api_v2.schemas.search_result import SearchResult, create_search_result from ..services.program_service import ProgramService _logger = logging.getLogger(__name__) diff --git a/spp_api_v2_programs/routers/program_filters.py b/spp_api_v2_programs/routers/program_filters.py new file mode 100644 index 000000000..6bee47d87 --- /dev/null +++ b/spp_api_v2_programs/routers/program_filters.py @@ -0,0 +1,80 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Filter discovery + advanced search endpoints for Program and ProgramMembership. + +Registers the Program / ProgramMembership resources into the shared +``RESOURCE_SERVICES`` registry defined in ``spp_api_v2`` and exposes their +``/_filters`` and ``/_search`` endpoints. The endpoint factories are reused +from the base filter module so behaviour is identical to the pre-split +implementation (OP#1081). +""" + +from odoo.addons.spp_api_v2.routers.filter import ( + RESOURCE_SERVICES, + _create_filter_metadata_endpoint, + _create_search_endpoint, +) +from odoo.addons.spp_api_v2.schemas.bundle import Bundle +from odoo.addons.spp_api_v2.schemas.filter import FilterMetadataResponse + +from fastapi import APIRouter + +from ..services.program_membership_service import ProgramMembershipService +from ..services.program_service import ProgramService + +# Register program resources into the shared filter registry so the generic +# search / filter factories resolve them at request time. +RESOURCE_SERVICES["Program"] = { + "service_class": ProgramService, + "model": "spp.program", + "base_domain": [], + "consent_type": None, +} +RESOURCE_SERVICES["ProgramMembership"] = { + "service_class": ProgramMembershipService, + "model": "spp.program.membership", + "base_domain": [], + "consent_type": "program_membership", +} + +program_filter_router = APIRouter(tags=["Program"], prefix="/Program") +program_membership_filter_router = APIRouter(tags=["ProgramMembership"], prefix="/ProgramMembership") + +# Register endpoints for Program +program_filter_router.add_api_route( + "/_filters", + _create_filter_metadata_endpoint("Program"), + methods=["GET"], + response_model=FilterMetadataResponse, + response_model_exclude_none=True, + summary="Get Program Filters", + description="Get available filters and presets for Program resource", +) +program_filter_router.add_api_route( + "/_search", + _create_search_endpoint("Program"), + methods=["POST"], + response_model=Bundle, + response_model_exclude_none=True, + summary="Advanced Program Search", + description="Search programs with complex filter conditions", +) + +# Register endpoints for ProgramMembership +program_membership_filter_router.add_api_route( + "/_filters", + _create_filter_metadata_endpoint("ProgramMembership"), + methods=["GET"], + response_model=FilterMetadataResponse, + response_model_exclude_none=True, + summary="Get ProgramMembership Filters", + description="Get available filters and presets for ProgramMembership resource", +) +program_membership_filter_router.add_api_route( + "/_search", + _create_search_endpoint("ProgramMembership"), + methods=["POST"], + response_model=Bundle, + response_model_exclude_none=True, + summary="Advanced ProgramMembership Search", + description="Search program memberships with complex filter conditions", +) diff --git a/spp_api_v2/routers/program_membership.py b/spp_api_v2_programs/routers/program_membership.py similarity index 97% rename from spp_api_v2/routers/program_membership.py rename to spp_api_v2_programs/routers/program_membership.py index 4830ca502..07022aca3 100644 --- a/spp_api_v2/routers/program_membership.py +++ b/spp_api_v2_programs/routers/program_membership.py @@ -20,12 +20,12 @@ status, ) -from ..middleware.auth import get_authenticated_client +from odoo.addons.spp_api_v2.middleware.auth import get_authenticated_client from ..schemas.program_membership import ProgramMembership -from ..schemas.search_result import SearchResult, create_search_result -from ..services.consent_service import ConsentService +from odoo.addons.spp_api_v2.schemas.search_result import SearchResult, create_search_result +from odoo.addons.spp_api_v2.services.consent_service import ConsentService from ..services.program_membership_service import ProgramMembershipService -from ..utils.pagination import fetch_with_consent +from odoo.addons.spp_api_v2.utils.pagination import fetch_with_consent _logger = logging.getLogger(__name__) diff --git a/spp_api_v2_programs/schemas/__init__.py b/spp_api_v2_programs/schemas/__init__.py new file mode 100644 index 000000000..aeece600d --- /dev/null +++ b/spp_api_v2_programs/schemas/__init__.py @@ -0,0 +1,4 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import program +from . import program_membership diff --git a/spp_api_v2/schemas/program.py b/spp_api_v2_programs/schemas/program.py similarity index 96% rename from spp_api_v2/schemas/program.py rename to spp_api_v2_programs/schemas/program.py index 173009de5..f81252bc1 100644 --- a/spp_api_v2/schemas/program.py +++ b/spp_api_v2_programs/schemas/program.py @@ -5,7 +5,7 @@ from pydantic import BaseModel, ConfigDict, Field -from .base import CodeableConcept, Identifier, Period, ResourceMeta +from odoo.addons.spp_api_v2.schemas.base import CodeableConcept, Identifier, Period, ResourceMeta class Program(BaseModel): diff --git a/spp_api_v2/schemas/program_membership.py b/spp_api_v2_programs/schemas/program_membership.py similarity index 96% rename from spp_api_v2/schemas/program_membership.py rename to spp_api_v2_programs/schemas/program_membership.py index 710e5e84a..29810fc69 100644 --- a/spp_api_v2/schemas/program_membership.py +++ b/spp_api_v2_programs/schemas/program_membership.py @@ -6,7 +6,7 @@ from pydantic import BaseModel, ConfigDict, Field -from .base import CodeableConcept, Identifier, Reference, ResourceMeta +from odoo.addons.spp_api_v2.schemas.base import CodeableConcept, Identifier, Reference, ResourceMeta class ProgramMembership(BaseModel): diff --git a/spp_api_v2_programs/security/ir.model.access.csv b/spp_api_v2_programs/security/ir.model.access.csv new file mode 100644 index 000000000..97dd8b917 --- /dev/null +++ b/spp_api_v2_programs/security/ir.model.access.csv @@ -0,0 +1 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink diff --git a/spp_api_v2_programs/services/__init__.py b/spp_api_v2_programs/services/__init__.py new file mode 100644 index 000000000..1dcbdd006 --- /dev/null +++ b/spp_api_v2_programs/services/__init__.py @@ -0,0 +1,4 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import program_service +from . import program_membership_service diff --git a/spp_api_v2/services/program_membership_service.py b/spp_api_v2_programs/services/program_membership_service.py similarity index 100% rename from spp_api_v2/services/program_membership_service.py rename to spp_api_v2_programs/services/program_membership_service.py diff --git a/spp_api_v2/services/program_service.py b/spp_api_v2_programs/services/program_service.py similarity index 100% rename from spp_api_v2/services/program_service.py rename to spp_api_v2_programs/services/program_service.py diff --git a/spp_api_v2_programs/static/description/icon.png b/spp_api_v2_programs/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c7dbdaaf1dace8f0ccf8c2087047ddfcf584af0c GIT binary patch literal 15480 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 + + + + +OpenSPP API V2 - Programs + + + +
+

OpenSPP API V2 - Programs

+ + +

Production/Stable License: LGPL-3 OpenSPP/OpenSPP2

+

Extends OpenSPP API V2 with REST endpoints for Programs and +Program Memberships. This module was split out of spp_api_v2 so +that the base API module no longer depends on spp_programs — +deployments that don’t use programs (e.g. a registry-only Social +Registry) can ship the API without installing the Programs stack.

+

It auto-installs whenever both spp_api_v2 and spp_programs are +present, so program API functionality is unchanged for deployments that +use programs.

+
+

Key Capabilities

+
    +
  • Program endpoints: read and search programs +(/api/v2/spp/Program)
  • +
  • Program Membership endpoints: read, search, create, and update +beneficiary enrollments (/api/v2/spp/ProgramMembership)
  • +
  • Advanced filtering: /_filters and /_search endpoints for +both resources, registered into the shared API V2 filter framework
  • +
  • OAuth scopes: the program and program_membership +client-scope resources (defined in spp_api_v2) gate access
  • +
+
+
+

UI Location

+

No standalone menu. Endpoints are available under the API V2 app:

+
    +
  • /api/v2/spp/Program, /api/v2/spp/Program/{identifier}
  • +
  • /api/v2/spp/ProgramMembership, +/api/v2/spp/ProgramMembership/{identifier}
  • +
+
+
+

Dependencies

+

spp_api_v2, spp_programs

+

Table of contents

+
+ +
+ +
+
+

19.0.1.0.0

+
    +
  • Split program and program-membership REST endpoints out of +spp_api_v2 into this companion module (#1081), so spp_api_v2 +no longer depends on spp_programs.
  • +
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • OpenSPP.org
  • +
+
+
+

Maintainers

+

Current maintainers:

+

jeremi gonzalesedwin1123 emjay0921

+

This module is part of the OpenSPP/OpenSPP2 project on GitHub.

+

You are welcome to contribute.

+
+
+
+
+ + diff --git a/spp_api_v2_programs/tests/__init__.py b/spp_api_v2_programs/tests/__init__.py new file mode 100644 index 000000000..c8d2b6f67 --- /dev/null +++ b/spp_api_v2_programs/tests/__init__.py @@ -0,0 +1,7 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import test_program_api +from . import test_program_membership_api +from . import test_program_service +from . import test_program_membership_service +from . import test_scope_enforcement_program diff --git a/spp_api_v2/tests/test_program_api.py b/spp_api_v2_programs/tests/test_program_api.py similarity index 99% rename from spp_api_v2/tests/test_program_api.py rename to spp_api_v2_programs/tests/test_program_api.py index cbe108f71..98300f7c6 100644 --- a/spp_api_v2/tests/test_program_api.py +++ b/spp_api_v2_programs/tests/test_program_api.py @@ -3,7 +3,7 @@ import json -from .common import ApiV2HttpTestCase +from odoo.addons.spp_api_v2.tests.common import ApiV2HttpTestCase class TestProgramAPIEndpoints(ApiV2HttpTestCase): diff --git a/spp_api_v2/tests/test_program_membership_api.py b/spp_api_v2_programs/tests/test_program_membership_api.py similarity index 99% rename from spp_api_v2/tests/test_program_membership_api.py rename to spp_api_v2_programs/tests/test_program_membership_api.py index 17612904a..639eb8b72 100644 --- a/spp_api_v2/tests/test_program_membership_api.py +++ b/spp_api_v2_programs/tests/test_program_membership_api.py @@ -4,7 +4,7 @@ import json from datetime import date -from .common import ApiV2HttpTestCase +from odoo.addons.spp_api_v2.tests.common import ApiV2HttpTestCase class TestProgramMembershipAPIEndpoints(ApiV2HttpTestCase): diff --git a/spp_api_v2/tests/test_program_membership_service.py b/spp_api_v2_programs/tests/test_program_membership_service.py similarity index 99% rename from spp_api_v2/tests/test_program_membership_service.py rename to spp_api_v2_programs/tests/test_program_membership_service.py index e245140a2..efea3bb5c 100644 --- a/spp_api_v2/tests/test_program_membership_service.py +++ b/spp_api_v2_programs/tests/test_program_membership_service.py @@ -5,10 +5,10 @@ from odoo.exceptions import ValidationError -from ..schemas.base import Reference +from odoo.addons.spp_api_v2.schemas.base import Reference from ..schemas.program_membership import ProgramMembership from ..services.program_membership_service import ProgramMembershipService -from .common import ApiV2TestCase +from odoo.addons.spp_api_v2.tests.common import ApiV2TestCase class TestProgramMembershipService(ApiV2TestCase): diff --git a/spp_api_v2/tests/test_program_service.py b/spp_api_v2_programs/tests/test_program_service.py similarity index 99% rename from spp_api_v2/tests/test_program_service.py rename to spp_api_v2_programs/tests/test_program_service.py index e1e00f338..b072c79ff 100644 --- a/spp_api_v2/tests/test_program_service.py +++ b/spp_api_v2_programs/tests/test_program_service.py @@ -2,7 +2,7 @@ """Tests for ProgramService""" from ..services.program_service import ProgramService -from .common import ApiV2TestCase +from odoo.addons.spp_api_v2.tests.common import ApiV2TestCase class TestProgramService(ApiV2TestCase): diff --git a/spp_api_v2_programs/tests/test_scope_enforcement_program.py b/spp_api_v2_programs/tests/test_scope_enforcement_program.py new file mode 100644 index 000000000..8aff7549c --- /dev/null +++ b/spp_api_v2_programs/tests/test_scope_enforcement_program.py @@ -0,0 +1,104 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Scope enforcement tests for the Program Membership endpoints. + +Moved out of spp_api_v2 together with the program API (OP#1081). +""" + +import json + +from odoo.addons.spp_api_v2.tests.common import ApiV2HttpTestCase + + +class TestScopeEnforcementProgramMembership(ApiV2HttpTestCase): + """Test scope enforcement on Program Membership endpoints""" + + def setUp(self): + super().setUp() + # Create test data + self.program = self.create_test_program(name="Test Program", target_type="individual") + self.individual = self.create_test_individual(identifier_value="SCOPE-PM-001") + + # Create test membership + self.membership = self.create_test_membership( + partner=self.individual, + program=self.program, + state="enrolled", + ) + + # Base URL for program membership + self.pm_url = "/api/v2/spp/ProgramMembership" + self.pm_id_url = f"{self.pm_url}/urn:openspp:vocab:id-type%23test_national_id|SCOPE-PM-001" + + def _make_client_without_scope(self, excluded_resource, excluded_action): + """Create a client that has all scopes EXCEPT the specified one""" + scopes = [] + # Give it a different resource scope to prove it's not resource-agnostic + other_resource = "individual" if excluded_resource == "program_membership" else "program_membership" + scopes.append({"resource": other_resource, "action": "all"}) + client = self.create_api_client( + name=f"No {excluded_resource}:{excluded_action}", + scopes=scopes, + require_consent=False, + ) + return client, self.generate_jwt_token(client) + + def test_program_membership_create_requires_scope(self): + """POST /ProgramMembership returns 403 without program_membership:create scope""" + client, token = self._make_client_without_scope("program_membership", "create") + + # Create new individual for enrollment + self.create_test_individual(identifier_value="SCOPE-PM-NEW") + + payload = { + "resourceType": "ProgramMembership", + "status": "enrolled", + "beneficiary": { + "reference": "Individual/urn:openspp:vocab:id-type#test_national_id|SCOPE-PM-NEW", + }, + "program": { + "reference": "Program/urn:openspp:program|test-program", + }, + } + + response = self.url_open( + self.pm_url, + data=json.dumps(payload), + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {token}", + }, + ) + + self.assertEqual(response.status_code, 403) + data = json.loads(response.content) + self.assertIn("Missing required scope", data["detail"]) + self.assertIn("program_membership:create", data["detail"]) + + def test_program_membership_update_requires_scope(self): + """PUT /ProgramMembership/{id} returns 403 without program_membership:update scope""" + client, token = self._make_client_without_scope("program_membership", "update") + + payload = { + "resourceType": "ProgramMembership", + "status": "paused", + "beneficiary": { + "reference": "Individual/urn:openspp:vocab:id-type#test_national_id|SCOPE-PM-001", + }, + "program": { + "reference": "Program/urn:openspp:program|test-program", + }, + } + + response = self.url_put( + self.pm_id_url, + data=json.dumps(payload), + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {token}", + }, + ) + + self.assertEqual(response.status_code, 403) + data = json.loads(response.content) + self.assertIn("Missing required scope", data["detail"]) + self.assertIn("program_membership:update", data["detail"]) From 9777bfbdbebb006ec70358dc9484f0194cff5ce0 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Tue, 9 Jun 2026 13:18:17 +0800 Subject: [PATCH 2/8] feat(studio): split program scoping into spp_studio_programs (#1083) Remove the two hard spp.program references from spp_studio so it no longer DIRECTLY depends on spp_programs: - program_ids (Many2many spp.program) on spp.studio.mixin - program_id (Many2one spp.program) on the pack-install wizard Both move to a new spp_studio_programs companion (auto-installs when spp_studio + spp_programs are both present). The base wizard now resolves the program via a _get_pack_program_id() hook (None in base; the companion returns the selected program). Note: spp_studio still pulls spp_programs transitively via spp_audit, which also depends on spp_programs and is not yet split. --- spp_studio/__manifest__.py | 1 - spp_studio/models/studio_mixin.py | 6 - spp_studio/tests/test_studio_mixin.py | 5 - spp_studio/wizard/pack_install_wizard.py | 16 +- spp_studio_programs/README.rst | 100 ++++ spp_studio_programs/__init__.py | 3 + spp_studio_programs/__manifest__.py | 26 ++ spp_studio_programs/models/__init__.py | 4 + .../models/pack_install_wizard.py | 24 + spp_studio_programs/models/studio_mixin.py | 20 + spp_studio_programs/pyproject.toml | 3 + spp_studio_programs/readme/DESCRIPTION.md | 12 + spp_studio_programs/readme/HISTORY.md | 3 + .../security/ir.model.access.csv | 1 + .../static/description/icon.png | Bin 0 -> 15480 bytes .../static/description/index.html | 437 ++++++++++++++++++ spp_studio_programs/tests/__init__.py | 3 + .../tests/test_studio_mixin_programs.py | 13 + 18 files changed, 659 insertions(+), 18 deletions(-) create mode 100644 spp_studio_programs/README.rst create mode 100644 spp_studio_programs/__init__.py create mode 100644 spp_studio_programs/__manifest__.py create mode 100644 spp_studio_programs/models/__init__.py create mode 100644 spp_studio_programs/models/pack_install_wizard.py create mode 100644 spp_studio_programs/models/studio_mixin.py create mode 100644 spp_studio_programs/pyproject.toml create mode 100644 spp_studio_programs/readme/DESCRIPTION.md create mode 100644 spp_studio_programs/readme/HISTORY.md create mode 100644 spp_studio_programs/security/ir.model.access.csv create mode 100644 spp_studio_programs/static/description/icon.png create mode 100644 spp_studio_programs/static/description/index.html create mode 100644 spp_studio_programs/tests/__init__.py create mode 100644 spp_studio_programs/tests/test_studio_mixin_programs.py diff --git a/spp_studio/__manifest__.py b/spp_studio/__manifest__.py index ea14eb2ce..dab2e9e6d 100644 --- a/spp_studio/__manifest__.py +++ b/spp_studio/__manifest__.py @@ -14,7 +14,6 @@ "spp_security", "spp_registry", "spp_base_common", - "spp_programs", "spp_user_roles", "spp_custom_field", "spp_cel_domain", # Unified variable system diff --git a/spp_studio/models/studio_mixin.py b/spp_studio/models/studio_mixin.py index 2c6c11de9..4580ff60d 100644 --- a/spp_studio/models/studio_mixin.py +++ b/spp_studio/models/studio_mixin.py @@ -78,12 +78,6 @@ class StudioMixin(models.AbstractModel): copy=False, ) - # Link to programs (optional, for program-specific configs) - program_ids = fields.Many2many( - "spp.program", - string="Programs", - help="If set, this configuration is only visible in these programs. Leave empty for global visibility.", - ) def action_activate(self): """Activate the configuration, making it available for use.""" diff --git a/spp_studio/tests/test_studio_mixin.py b/spp_studio/tests/test_studio_mixin.py index 091e2d3c2..b284f3e5c 100644 --- a/spp_studio/tests/test_studio_mixin.py +++ b/spp_studio/tests/test_studio_mixin.py @@ -42,8 +42,3 @@ def test_audit_fields_defined(self): self.assertIn("deactivated_by_id", fields) self.assertIn("deactivated_date", fields) - def test_program_ids_field(self): - """Test program_ids field for program-specific configurations.""" - self.assertIn("program_ids", self.StudioField._fields) - field = self.StudioField._fields["program_ids"] - self.assertEqual(field.type, "many2many") diff --git a/spp_studio/wizard/pack_install_wizard.py b/spp_studio/wizard/pack_install_wizard.py index 08458d6ef..eeb28eaab 100644 --- a/spp_studio/wizard/pack_install_wizard.py +++ b/spp_studio/wizard/pack_install_wizard.py @@ -64,11 +64,6 @@ class PackInstallWizard(models.TransientModel): string="Show Preview", default=False, ) - program_id = fields.Many2one( - "spp.program", - string="Program", - help="Optional: Program for constant value lookups", - ) # Results result_message = fields.Text(string="Result", readonly=True) @@ -115,6 +110,15 @@ def _compute_missing_variables(self): wizard.missing_variables = _("All required variables are available.") wizard.has_missing_variables = False + def _get_pack_program_id(self): + """Program id used for constant-value lookups during pack expansion. + + None in the base module. The ``spp_studio_programs`` companion adds a + ``program_id`` field and returns it here, so program-scoped lookups work + without ``spp_studio`` depending on ``spp_programs`` (OP#1083). + """ + return None + def action_preview(self): """Generate preview showing original expressions and runtime resolution preview. @@ -136,7 +140,7 @@ def action_preview(self): previews = [] for item in self.item_ids: - result = resolver.expand_pack_item(item, program_id=self.program_id.id if self.program_id else None) + result = resolver.expand_pack_item(item, program_id=self._get_pack_program_id()) previews.append( Command.create( diff --git a/spp_studio_programs/README.rst b/spp_studio_programs/README.rst new file mode 100644 index 000000000..1f7bce2e4 --- /dev/null +++ b/spp_studio_programs/README.rst @@ -0,0 +1,100 @@ +========================= +OpenSPP Studio - Programs +========================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:e722af971f449aa84db11c19b7ea7ef4463d86350e8972b00c0d4beb086586a1 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OpenSPP%2FOpenSPP2-lightgray.png?logo=github + :target: https://github.com/OpenSPP/OpenSPP2/tree/19.0/spp_studio_programs + :alt: OpenSPP/OpenSPP2 + +|badge1| |badge2| |badge3| + +Adds program scoping to OpenSPP Studio (no-code) configurations. Split +out of ``spp_studio`` so the base Studio module no longer depends on +``spp_programs`` — deployments without programs can use Studio without +installing the Programs stack. + +Auto-installs whenever both ``spp_studio`` and ``spp_programs`` are +present, so program-scoping behaviour is unchanged where programs are +used. + +Key Capabilities +~~~~~~~~~~~~~~~~ + +- Adds the optional ``program_ids`` link on Studio configurations + (``spp.studio.mixin``), so fields / logic variables can be scoped to + specific programs +- Adds the optional ``program_id`` on the Logic Pack install wizard for + program-specific constant-value lookups + +Dependencies +~~~~~~~~~~~~ + +``spp_studio``, ``spp_programs`` + +**Table of contents** + +.. contents:: + :local: + +Changelog +========= + +19.0.1.0.0 +~~~~~~~~~~ + +- Split program scoping (config ``program_ids`` and the pack-install + wizard's ``program_id``) out of ``spp_studio`` into this companion + (#1083), so ``spp_studio`` no longer depends on ``spp_programs``. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* OpenSPP.org + +Maintainers +----------- + +.. |maintainer-jeremi| image:: https://github.com/jeremi.png?size=40px + :target: https://github.com/jeremi + :alt: jeremi +.. |maintainer-gonzalesedwin1123| image:: https://github.com/gonzalesedwin1123.png?size=40px + :target: https://github.com/gonzalesedwin1123 + :alt: gonzalesedwin1123 +.. |maintainer-emjay0921| image:: https://github.com/emjay0921.png?size=40px + :target: https://github.com/emjay0921 + :alt: emjay0921 + +Current maintainers: + +|maintainer-jeremi| |maintainer-gonzalesedwin1123| |maintainer-emjay0921| + +This module is part of the `OpenSPP/OpenSPP2 `_ project on GitHub. + +You are welcome to contribute. \ No newline at end of file diff --git a/spp_studio_programs/__init__.py b/spp_studio_programs/__init__.py new file mode 100644 index 000000000..c4ccea794 --- /dev/null +++ b/spp_studio_programs/__init__.py @@ -0,0 +1,3 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/spp_studio_programs/__manifest__.py b/spp_studio_programs/__manifest__.py new file mode 100644 index 000000000..a57a5c3a3 --- /dev/null +++ b/spp_studio_programs/__manifest__.py @@ -0,0 +1,26 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +{ # pylint: disable=pointless-statement + "name": "OpenSPP Studio - Programs", + "category": "OpenSPP/Configuration", + "version": "19.0.1.0.0", + "sequence": 1, + "author": "OpenSPP.org", + "website": "https://github.com/OpenSPP/OpenSPP2", + "license": "LGPL-3", + "development_status": "Production/Stable", + "maintainers": ["jeremi", "gonzalesedwin1123", "emjay0921"], + "summary": "Program scoping for OpenSPP Studio configurations.", + "depends": [ + "spp_studio", + "spp_programs", + ], + "data": [ + "security/ir.model.access.csv", + ], + "assets": {}, + "demo": [], + "images": [], + "application": False, + "installable": True, + "auto_install": ["spp_studio", "spp_programs"], +} diff --git a/spp_studio_programs/models/__init__.py b/spp_studio_programs/models/__init__.py new file mode 100644 index 000000000..1f0fcb24c --- /dev/null +++ b/spp_studio_programs/models/__init__.py @@ -0,0 +1,4 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import studio_mixin +from . import pack_install_wizard diff --git a/spp_studio_programs/models/pack_install_wizard.py b/spp_studio_programs/models/pack_install_wizard.py new file mode 100644 index 000000000..ebcba5941 --- /dev/null +++ b/spp_studio_programs/models/pack_install_wizard.py @@ -0,0 +1,24 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Add program selection to the Logic Pack install wizard. + +Re-opens ``spp.studio.pack.install.wizard`` to add the optional ``program_id`` +used for constant-value lookups during pack expansion, and supplies it through +the ``_get_pack_program_id`` hook defined on the base wizard (OP#1083). +""" + +from odoo import fields, models + + +class PackInstallWizard(models.TransientModel): + _inherit = "spp.studio.pack.install.wizard" + + program_id = fields.Many2one( + "spp.program", + string="Program", + help="Optional: Program for constant value lookups", + ) + + def _get_pack_program_id(self): + """Use the selected program for constant-value lookups.""" + self.ensure_one() + return self.program_id.id if self.program_id else None diff --git a/spp_studio_programs/models/studio_mixin.py b/spp_studio_programs/models/studio_mixin.py new file mode 100644 index 000000000..2caaf8a41 --- /dev/null +++ b/spp_studio_programs/models/studio_mixin.py @@ -0,0 +1,20 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Add program-scoping to Studio configurations. + +Re-opens ``spp.studio.mixin`` to add the optional ``program_ids`` link, so +Studio configs (fields, logic variables) can be scoped to specific programs. +This lives in the companion so the base ``spp_studio`` module does not depend +on ``spp_programs`` (OP#1083). +""" + +from odoo import fields, models + + +class StudioMixin(models.AbstractModel): + _inherit = "spp.studio.mixin" + + program_ids = fields.Many2many( + "spp.program", + string="Programs", + help="If set, this configuration is only visible in these programs. Leave empty for global visibility.", + ) diff --git a/spp_studio_programs/pyproject.toml b/spp_studio_programs/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/spp_studio_programs/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/spp_studio_programs/readme/DESCRIPTION.md b/spp_studio_programs/readme/DESCRIPTION.md new file mode 100644 index 000000000..6298a2b90 --- /dev/null +++ b/spp_studio_programs/readme/DESCRIPTION.md @@ -0,0 +1,12 @@ +Adds program scoping to OpenSPP Studio (no-code) configurations. Split out of `spp_studio` so the base Studio module no longer depends on `spp_programs` — deployments without programs can use Studio without installing the Programs stack. + +Auto-installs whenever both `spp_studio` and `spp_programs` are present, so program-scoping behaviour is unchanged where programs are used. + +### Key Capabilities + +- Adds the optional `program_ids` link on Studio configurations (`spp.studio.mixin`), so fields / logic variables can be scoped to specific programs +- Adds the optional `program_id` on the Logic Pack install wizard for program-specific constant-value lookups + +### Dependencies + +`spp_studio`, `spp_programs` diff --git a/spp_studio_programs/readme/HISTORY.md b/spp_studio_programs/readme/HISTORY.md new file mode 100644 index 000000000..d6a37b0bd --- /dev/null +++ b/spp_studio_programs/readme/HISTORY.md @@ -0,0 +1,3 @@ +### 19.0.1.0.0 + +- Split program scoping (config ``program_ids`` and the pack-install wizard's ``program_id``) out of `spp_studio` into this companion (#1083), so `spp_studio` no longer depends on `spp_programs`. diff --git a/spp_studio_programs/security/ir.model.access.csv b/spp_studio_programs/security/ir.model.access.csv new file mode 100644 index 000000000..97dd8b917 --- /dev/null +++ b/spp_studio_programs/security/ir.model.access.csv @@ -0,0 +1 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink diff --git a/spp_studio_programs/static/description/icon.png b/spp_studio_programs/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c7dbdaaf1dace8f0ccf8c2087047ddfcf584af0c GIT binary patch literal 15480 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 + + + + +OpenSPP Studio - Programs + + + +
+

OpenSPP Studio - Programs

+ + +

Production/Stable License: LGPL-3 OpenSPP/OpenSPP2

+

Adds program scoping to OpenSPP Studio (no-code) configurations. Split +out of spp_studio so the base Studio module no longer depends on +spp_programs — deployments without programs can use Studio without +installing the Programs stack.

+

Auto-installs whenever both spp_studio and spp_programs are +present, so program-scoping behaviour is unchanged where programs are +used.

+
+

Key Capabilities

+
    +
  • Adds the optional program_ids link on Studio configurations +(spp.studio.mixin), so fields / logic variables can be scoped to +specific programs
  • +
  • Adds the optional program_id on the Logic Pack install wizard for +program-specific constant-value lookups
  • +
+
+
+

Dependencies

+

spp_studio, spp_programs

+

Table of contents

+
+ +
+ +
+
+

19.0.1.0.0

+
    +
  • Split program scoping (config program_ids and the pack-install +wizard’s program_id) out of spp_studio into this companion +(#1083), so spp_studio no longer depends on spp_programs.
  • +
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • OpenSPP.org
  • +
+
+
+

Maintainers

+

Current maintainers:

+

jeremi gonzalesedwin1123 emjay0921

+

This module is part of the OpenSPP/OpenSPP2 project on GitHub.

+

You are welcome to contribute.

+
+
+
+
+ + diff --git a/spp_studio_programs/tests/__init__.py b/spp_studio_programs/tests/__init__.py new file mode 100644 index 000000000..fbc72b00f --- /dev/null +++ b/spp_studio_programs/tests/__init__.py @@ -0,0 +1,3 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import test_studio_mixin_programs diff --git a/spp_studio_programs/tests/test_studio_mixin_programs.py b/spp_studio_programs/tests/test_studio_mixin_programs.py new file mode 100644 index 000000000..06bcaa895 --- /dev/null +++ b/spp_studio_programs/tests/test_studio_mixin_programs.py @@ -0,0 +1,13 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""The program_ids scoping field is added to Studio configs by this companion.""" + +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestStudioMixinPrograms(TransactionCase): + def test_program_ids_field(self): + """program_ids many2many is present on Studio configurations.""" + StudioField = self.env["spp.studio.field"] + self.assertIn("program_ids", StudioField._fields) + self.assertEqual(StudioField._fields["program_ids"].type, "many2many") From c4930e39100c386b5ce8280c0f718508c6f309c0 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Tue, 9 Jun 2026 13:40:43 +0800 Subject: [PATCH 3/8] feat(source_tracking): split program-membership tracking into spp_source_tracking_programs (#1084) Move the spp.program.membership source-tracking extension out of spp_source_tracking into a new spp_source_tracking_programs companion (auto-installs when spp_source_tracking + spp_programs are both present), so spp_source_tracking no longer depends on spp_programs. - Base res_partner merge logic already guards program_membership_ids via hasattr, so it stays in the base module. - Guarded the 1.0 migration's spp_program_membership UPDATE behind a table-existence check. - Base source-tracking tests skip the program-membership cases when spp.program is absent; the companion affirms the extension. With #1081 + #1084, spp_api_v2, spp_api_v2_data and spp_source_tracking now install with no spp_programs (verified). spp_studio still pulls it via spp_audit (tracked in #1085). --- spp_source_tracking/__manifest__.py | 2 +- .../migrations/1.0/post-migrate.py | 32 +- spp_source_tracking/models/__init__.py | 1 - .../tests/test_merge_provenance.py | 27 +- .../tests/test_source_tracking_mixin.py | 25 +- spp_source_tracking_programs/README.rst | 92 ++++ spp_source_tracking_programs/__init__.py | 3 + spp_source_tracking_programs/__manifest__.py | 26 ++ .../models/__init__.py | 3 + .../models/program_membership.py | 0 spp_source_tracking_programs/pyproject.toml | 3 + .../readme/DESCRIPTION.md | 7 + .../readme/HISTORY.md | 3 + .../security/ir.model.access.csv | 1 + .../static/description/icon.png | Bin 0 -> 15480 bytes .../static/description/index.html | 428 ++++++++++++++++++ .../tests/__init__.py | 3 + ...test_program_membership_source_tracking.py | 22 + 18 files changed, 645 insertions(+), 33 deletions(-) create mode 100644 spp_source_tracking_programs/README.rst create mode 100644 spp_source_tracking_programs/__init__.py create mode 100644 spp_source_tracking_programs/__manifest__.py create mode 100644 spp_source_tracking_programs/models/__init__.py rename {spp_source_tracking => spp_source_tracking_programs}/models/program_membership.py (100%) create mode 100644 spp_source_tracking_programs/pyproject.toml create mode 100644 spp_source_tracking_programs/readme/DESCRIPTION.md create mode 100644 spp_source_tracking_programs/readme/HISTORY.md create mode 100644 spp_source_tracking_programs/security/ir.model.access.csv create mode 100644 spp_source_tracking_programs/static/description/icon.png create mode 100644 spp_source_tracking_programs/static/description/index.html create mode 100644 spp_source_tracking_programs/tests/__init__.py create mode 100644 spp_source_tracking_programs/tests/test_program_membership_source_tracking.py diff --git a/spp_source_tracking/__manifest__.py b/spp_source_tracking/__manifest__.py index 372616de9..39f7cd3cc 100644 --- a/spp_source_tracking/__manifest__.py +++ b/spp_source_tracking/__manifest__.py @@ -11,7 +11,7 @@ "license": "LGPL-3", "development_status": "Production/Stable", "maintainers": ["OpenSPP"], - "depends": ["base", "spp_security", "spp_registry", "spp_programs"], + "depends": ["base", "spp_security", "spp_registry"], "data": [ "security/ir.model.access.csv", "views/merge_provenance_views.xml", diff --git a/spp_source_tracking/migrations/1.0/post-migrate.py b/spp_source_tracking/migrations/1.0/post-migrate.py index e4f45c5af..ec0674ac2 100644 --- a/spp_source_tracking/migrations/1.0/post-migrate.py +++ b/spp_source_tracking/migrations/1.0/post-migrate.py @@ -44,20 +44,26 @@ def migrate(cr, version): reg_id_count = cr.rowcount _logger.info("Updated %s existing registry IDs with migration source tracking", reg_id_count) - # Update existing program memberships + # Update existing program memberships — only when the programs stack is + # installed (the source-tracking extension now lives in + # spp_source_tracking_programs; the table may be absent otherwise). cr.execute( - """ - UPDATE spp_program_membership - SET source_system = 'v1-migration', - collection_method = 'migration', - collection_date = create_date - WHERE source_system IS NULL - """ - ) - membership_count = cr.rowcount - _logger.info( - "Updated %s existing program memberships with migration source tracking", - membership_count, + "SELECT 1 FROM information_schema.tables WHERE table_name = 'spp_program_membership'" ) + if cr.fetchone(): + cr.execute( + """ + UPDATE spp_program_membership + SET source_system = 'v1-migration', + collection_method = 'migration', + collection_date = create_date + WHERE source_system IS NULL + """ + ) + membership_count = cr.rowcount + _logger.info( + "Updated %s existing program memberships with migration source tracking", + membership_count, + ) _logger.info("Source tracking migration completed successfully") diff --git a/spp_source_tracking/models/__init__.py b/spp_source_tracking/models/__init__.py index bc2576fb5..d9b357a8f 100644 --- a/spp_source_tracking/models/__init__.py +++ b/spp_source_tracking/models/__init__.py @@ -4,4 +4,3 @@ from . import merge_provenance from . import res_partner from . import registry_id -from . import program_membership diff --git a/spp_source_tracking/tests/test_merge_provenance.py b/spp_source_tracking/tests/test_merge_provenance.py index 4c385001c..a320ff6c1 100644 --- a/spp_source_tracking/tests/test_merge_provenance.py +++ b/spp_source_tracking/tests/test_merge_provenance.py @@ -17,8 +17,12 @@ def setUpClass(cls): cls.RegistryId = cls.env["spp.registry.id"] cls.VocabularyCode = cls.env["spp.vocabulary.code"] cls.Vocabulary = cls.env["spp.vocabulary"] - cls.Program = cls.env["spp.program"] - cls.Membership = cls.env["spp.program.membership"] + # spp.program lives in spp_programs; program-membership merge handling + # is exercised only when the programs stack is installed. + cls.has_programs = "spp.program" in cls.env + if cls.has_programs: + cls.Program = cls.env["spp.program"] + cls.Membership = cls.env["spp.program.membership"] # Create a vocabulary for ID types (id_type_id expects spp.vocabulary.code) cls.test_vocab = cls.Vocabulary.create( @@ -36,13 +40,14 @@ def setUpClass(cls): } ) - # Create a test program with unique name - cls.program = cls.Program.create( - { - "name": f"Test Social Program {uuid.uuid4().hex[:8]}", - "target_type": "individual", - } - ) + # Create a test program with unique name (only when programs installed) + if cls.has_programs: + cls.program = cls.Program.create( + { + "name": f"Test Social Program {uuid.uuid4().hex[:8]}", + "target_type": "individual", + } + ) def _create_registrant(self, name, source_system="test-source", **kwargs): """Helper to create a registrant with source tracking.""" @@ -106,6 +111,8 @@ def test_merge_into_transfers_identifiers(self): def test_merge_into_transfers_memberships(self): """Test that merge_into transfers program memberships.""" + if not self.has_programs: + self.skipTest("spp.program not installed (see spp_source_tracking_programs)") source = self._create_registrant("Source Partner") target = self._create_registrant("Target Partner") @@ -123,6 +130,8 @@ def test_merge_into_transfers_memberships(self): def test_merge_into_handles_duplicate_memberships(self): """Test that duplicate memberships are archived during merge.""" + if not self.has_programs: + self.skipTest("spp.program not installed (see spp_source_tracking_programs)") source = self._create_registrant("Source Partner") target = self._create_registrant("Target Partner") diff --git a/spp_source_tracking/tests/test_source_tracking_mixin.py b/spp_source_tracking/tests/test_source_tracking_mixin.py index 38a869da7..4c3498664 100644 --- a/spp_source_tracking/tests/test_source_tracking_mixin.py +++ b/spp_source_tracking/tests/test_source_tracking_mixin.py @@ -17,8 +17,12 @@ def setUpClass(cls): cls.RegistryId = cls.env["spp.registry.id"] cls.VocabularyCode = cls.env["spp.vocabulary.code"] cls.Vocabulary = cls.env["spp.vocabulary"] - cls.Program = cls.env["spp.program"] - cls.Membership = cls.env["spp.program.membership"] + # spp.program lives in spp_programs; the program-membership source + # tracking now lives in the spp_source_tracking_programs companion. + cls.has_programs = "spp.program" in cls.env + if cls.has_programs: + cls.Program = cls.env["spp.program"] + cls.Membership = cls.env["spp.program.membership"] # Create a vocabulary for ID types (id_type_id expects spp.vocabulary.code) cls.test_vocab = cls.Vocabulary.create( @@ -36,13 +40,14 @@ def setUpClass(cls): } ) - # Create a test program with unique name - cls.program = cls.Program.create( - { - "name": f"Test Program {uuid.uuid4().hex[:8]}", - "target_type": "individual", - } - ) + # Create a test program with unique name (only when programs installed) + if cls.has_programs: + cls.program = cls.Program.create( + { + "name": f"Test Program {uuid.uuid4().hex[:8]}", + "target_type": "individual", + } + ) def test_create_sets_default_source_system(self): """Test that create sets default source_system to 'odoo-ui'.""" @@ -189,6 +194,8 @@ def test_registry_id_has_source_tracking(self): def test_program_membership_has_source_tracking(self): """Test that spp.program.membership inherits source tracking mixin.""" + if not self.has_programs: + self.skipTest("spp.program not installed (see spp_source_tracking_programs)") partner = self.Partner.create( { "name": "Test Partner", diff --git a/spp_source_tracking_programs/README.rst b/spp_source_tracking_programs/README.rst new file mode 100644 index 000000000..658d7850c --- /dev/null +++ b/spp_source_tracking_programs/README.rst @@ -0,0 +1,92 @@ +================================== +OpenSPP Source Tracking - Programs +================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:5193f25e638a9f42e8c7e55afd969616845d435904fc553977fa64b7fea0121a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OpenSPP%2FOpenSPP2-lightgray.png?logo=github + :target: https://github.com/OpenSPP/OpenSPP2/tree/19.0/spp_source_tracking_programs + :alt: OpenSPP/OpenSPP2 + +|badge1| |badge2| |badge3| + +Adds source/provenance tracking to program memberships +(``spp.program.membership``). Split out of ``spp_source_tracking`` so +the base source-tracking module no longer depends on ``spp_programs`` — +registry-only deployments get source tracking without the Programs +stack. + +Auto-installs whenever both ``spp_source_tracking`` and ``spp_programs`` +are present, so program-membership source tracking is unchanged where +programs are used. + +Dependencies +~~~~~~~~~~~~ + +``spp_source_tracking``, ``spp_programs`` + +**Table of contents** + +.. contents:: + :local: + +Changelog +========= + +19.0.1.0.0 +~~~~~~~~~~ + +- Split the program-membership source-tracking extension out of + ``spp_source_tracking`` into this companion (#1084), so + ``spp_source_tracking`` no longer depends on ``spp_programs``. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* OpenSPP.org + +Maintainers +----------- + +.. |maintainer-jeremi| image:: https://github.com/jeremi.png?size=40px + :target: https://github.com/jeremi + :alt: jeremi +.. |maintainer-gonzalesedwin1123| image:: https://github.com/gonzalesedwin1123.png?size=40px + :target: https://github.com/gonzalesedwin1123 + :alt: gonzalesedwin1123 +.. |maintainer-emjay0921| image:: https://github.com/emjay0921.png?size=40px + :target: https://github.com/emjay0921 + :alt: emjay0921 + +Current maintainers: + +|maintainer-jeremi| |maintainer-gonzalesedwin1123| |maintainer-emjay0921| + +This module is part of the `OpenSPP/OpenSPP2 `_ project on GitHub. + +You are welcome to contribute. \ No newline at end of file diff --git a/spp_source_tracking_programs/__init__.py b/spp_source_tracking_programs/__init__.py new file mode 100644 index 000000000..c4ccea794 --- /dev/null +++ b/spp_source_tracking_programs/__init__.py @@ -0,0 +1,3 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/spp_source_tracking_programs/__manifest__.py b/spp_source_tracking_programs/__manifest__.py new file mode 100644 index 000000000..bcef3fc30 --- /dev/null +++ b/spp_source_tracking_programs/__manifest__.py @@ -0,0 +1,26 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +{ # pylint: disable=pointless-statement + "name": "OpenSPP Source Tracking - Programs", + "category": "OpenSPP/Core", + "version": "19.0.1.0.0", + "sequence": 1, + "author": "OpenSPP.org", + "website": "https://github.com/OpenSPP/OpenSPP2", + "license": "LGPL-3", + "development_status": "Production/Stable", + "maintainers": ["jeremi", "gonzalesedwin1123", "emjay0921"], + "summary": "Source tracking for program memberships.", + "depends": [ + "spp_source_tracking", + "spp_programs", + ], + "data": [ + "security/ir.model.access.csv", + ], + "assets": {}, + "demo": [], + "images": [], + "application": False, + "installable": True, + "auto_install": ["spp_source_tracking", "spp_programs"], +} diff --git a/spp_source_tracking_programs/models/__init__.py b/spp_source_tracking_programs/models/__init__.py new file mode 100644 index 000000000..7cfbebd1a --- /dev/null +++ b/spp_source_tracking_programs/models/__init__.py @@ -0,0 +1,3 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import program_membership diff --git a/spp_source_tracking/models/program_membership.py b/spp_source_tracking_programs/models/program_membership.py similarity index 100% rename from spp_source_tracking/models/program_membership.py rename to spp_source_tracking_programs/models/program_membership.py diff --git a/spp_source_tracking_programs/pyproject.toml b/spp_source_tracking_programs/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/spp_source_tracking_programs/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/spp_source_tracking_programs/readme/DESCRIPTION.md b/spp_source_tracking_programs/readme/DESCRIPTION.md new file mode 100644 index 000000000..a255b6759 --- /dev/null +++ b/spp_source_tracking_programs/readme/DESCRIPTION.md @@ -0,0 +1,7 @@ +Adds source/provenance tracking to program memberships (`spp.program.membership`). Split out of `spp_source_tracking` so the base source-tracking module no longer depends on `spp_programs` — registry-only deployments get source tracking without the Programs stack. + +Auto-installs whenever both `spp_source_tracking` and `spp_programs` are present, so program-membership source tracking is unchanged where programs are used. + +### Dependencies + +`spp_source_tracking`, `spp_programs` diff --git a/spp_source_tracking_programs/readme/HISTORY.md b/spp_source_tracking_programs/readme/HISTORY.md new file mode 100644 index 000000000..30b26bfa7 --- /dev/null +++ b/spp_source_tracking_programs/readme/HISTORY.md @@ -0,0 +1,3 @@ +### 19.0.1.0.0 + +- Split the program-membership source-tracking extension out of `spp_source_tracking` into this companion (#1084), so `spp_source_tracking` no longer depends on `spp_programs`. diff --git a/spp_source_tracking_programs/security/ir.model.access.csv b/spp_source_tracking_programs/security/ir.model.access.csv new file mode 100644 index 000000000..97dd8b917 --- /dev/null +++ b/spp_source_tracking_programs/security/ir.model.access.csv @@ -0,0 +1 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink diff --git a/spp_source_tracking_programs/static/description/icon.png b/spp_source_tracking_programs/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c7dbdaaf1dace8f0ccf8c2087047ddfcf584af0c GIT binary patch literal 15480 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 + + + + +OpenSPP Source Tracking - Programs + + + +
+

OpenSPP Source Tracking - Programs

+ + +

Production/Stable License: LGPL-3 OpenSPP/OpenSPP2

+

Adds source/provenance tracking to program memberships +(spp.program.membership). Split out of spp_source_tracking so +the base source-tracking module no longer depends on spp_programs — +registry-only deployments get source tracking without the Programs +stack.

+

Auto-installs whenever both spp_source_tracking and spp_programs +are present, so program-membership source tracking is unchanged where +programs are used.

+
+

Dependencies

+

spp_source_tracking, spp_programs

+

Table of contents

+
+ +
+ +
+
+

19.0.1.0.0

+
    +
  • Split the program-membership source-tracking extension out of +spp_source_tracking into this companion (#1084), so +spp_source_tracking no longer depends on spp_programs.
  • +
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • OpenSPP.org
  • +
+
+
+

Maintainers

+

Current maintainers:

+

jeremi gonzalesedwin1123 emjay0921

+

This module is part of the OpenSPP/OpenSPP2 project on GitHub.

+

You are welcome to contribute.

+
+
+
+
+ + diff --git a/spp_source_tracking_programs/tests/__init__.py b/spp_source_tracking_programs/tests/__init__.py new file mode 100644 index 000000000..8e7ed3c81 --- /dev/null +++ b/spp_source_tracking_programs/tests/__init__.py @@ -0,0 +1,3 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import test_program_membership_source_tracking diff --git a/spp_source_tracking_programs/tests/test_program_membership_source_tracking.py b/spp_source_tracking_programs/tests/test_program_membership_source_tracking.py new file mode 100644 index 000000000..02ec50b4c --- /dev/null +++ b/spp_source_tracking_programs/tests/test_program_membership_source_tracking.py @@ -0,0 +1,22 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""spp.program.membership gains source tracking via this companion (OP#1084).""" + +import uuid + +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestProgramMembershipSourceTracking(TransactionCase): + def test_program_membership_has_source_tracking(self): + """spp.program.membership inherits the source-tracking mixin.""" + program = self.env["spp.program"].create( + {"name": f"Test Program {uuid.uuid4().hex[:8]}", "target_type": "individual"} + ) + partner = self.env["res.partner"].create({"name": "ST Partner", "is_registrant": True}) + membership = ( + self.env["spp.program.membership"] + .with_context(source_system="enrollment-api") + .create({"partner_id": partner.id, "program_id": program.id}) + ) + self.assertEqual(membership.source_system, "enrollment-api") From dd590ab4142d2ddfff900f9d208eca05ddb97c59 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Tue, 9 Jun 2026 13:52:19 +0800 Subject: [PATCH 4/8] feat(audit): split program audit rules into spp_audit_programs (#1085) Move the program/cycle audit rules (Program, Cycle, and the program manager rules) out of spp_audit into a new spp_audit_programs companion (auto-installs when spp_audit + spp_programs are both present), so spp_audit no longer depends on spp_programs. The base keeps only the Registry and Service Point rules. Also point the base self-protection / mail-thread audit tests at res.users instead of program models, so they pass without spp_programs. With this, spp_audit, spp_studio, spp_api_v2, spp_api_v2_data and spp_source_tracking all install with no spp_programs (verified: each installs alone with spp_programs uninstalled). Completes the #1080 decoupling. --- spp_audit/__manifest__.py | 1 - spp_audit/data/audit_rule_data.xml | 119 ----- spp_audit/tests/test_audit_backend.py | 6 +- spp_audit_programs/README.rst | 91 ++++ spp_audit_programs/__init__.py | 1 + spp_audit_programs/__manifest__.py | 26 ++ spp_audit_programs/data/audit_rule_data.xml | 125 +++++ spp_audit_programs/pyproject.toml | 3 + spp_audit_programs/readme/DESCRIPTION.md | 7 + spp_audit_programs/readme/HISTORY.md | 3 + .../static/description/icon.png | Bin 0 -> 13981 bytes .../static/description/index.html | 427 ++++++++++++++++++ spp_audit_programs/tests/__init__.py | 3 + .../tests/test_program_audit_rules.py | 13 + 14 files changed, 702 insertions(+), 123 deletions(-) create mode 100644 spp_audit_programs/README.rst create mode 100644 spp_audit_programs/__init__.py create mode 100644 spp_audit_programs/__manifest__.py create mode 100644 spp_audit_programs/data/audit_rule_data.xml create mode 100644 spp_audit_programs/pyproject.toml create mode 100644 spp_audit_programs/readme/DESCRIPTION.md create mode 100644 spp_audit_programs/readme/HISTORY.md create mode 100644 spp_audit_programs/static/description/icon.png create mode 100644 spp_audit_programs/static/description/index.html create mode 100644 spp_audit_programs/tests/__init__.py create mode 100644 spp_audit_programs/tests/test_program_audit_rules.py diff --git a/spp_audit/__manifest__.py b/spp_audit/__manifest__.py index 5b5bd4bf0..bef9d14a0 100644 --- a/spp_audit/__manifest__.py +++ b/spp_audit/__manifest__.py @@ -15,7 +15,6 @@ "mail", "spp_registry", "spp_security", - "spp_programs", "spp_service_points", ], "external_dependencies": { diff --git a/spp_audit/data/audit_rule_data.xml b/spp_audit/data/audit_rule_data.xml index 366a174bb..5c2ecbc4f 100644 --- a/spp_audit/data/audit_rule_data.xml +++ b/spp_audit/data/audit_rule_data.xml @@ -1,22 +1,5 @@ - - - Program Rule - spp.program - - - - - - Cycle Rule - spp.cycle - - - Registry Rule @@ -36,106 +19,4 @@ eval="['name', 'agent_home_address', 'agent_number', 'area_id', 'country_id', 'disabled_date', 'disabled_reason', 'display_name', 'dms_directory_ids', 'final_agent_number', 'id', 'individual_ids', 'is_contract_active', 'is_disabled', 'is_tamawwon_onboarded', 'message_attachment_count', 'message_follower_ids', 'message_has_error', 'message_has_error_counter', 'message_has_sms_error', 'message_is_follower', 'message_main_attachment_id', 'message_needaction', 'message_needaction_counter', 'message_partner_ids', 'message_unread', 'message_unread_counter', 'phone_no', 'phone_sanitized', 'res_partner_company_id', 'service_type_id', 'service_type_ids', 'shop_address', 'write_uid', 'website_message_ids', ]" /> - - - - Default Eligibility Manager Rule - spp.program.membership.manager.default - - - Program Rule - program_id - - - - - Deduplication Manager Rule - spp.deduplication.manager.default - - - Program Rule - program_id - - - - - Notification Manager Rule - spp.program.notification.manager.sms - - - Program Rule - program_id - - - - - Program Manager Rule - spp.program.manager.default - - - Program Rule - program_id - - - - - Cycle Manager Rule - spp.cycle.manager.default - - - Program Rule - program_id - - - - - Entitlement Manager Rule - spp.program.entitlement.manager.default - - - Program Rule - program_id - - - - - Payment Manager Rule - spp.program.payment.manager.default - - - Program Rule - program_id - - - - - Cash Entitlement Manager Rule - spp.program.entitlement.manager.cash - - - Program Rule - program_id - - - - - Basket Entitlement Manager Rule - spp.program.entitlement.manager.basket - - - Program Rule - program_id - diff --git a/spp_audit/tests/test_audit_backend.py b/spp_audit/tests/test_audit_backend.py index 2b36f80f2..9c050cc83 100644 --- a/spp_audit/tests/test_audit_backend.py +++ b/spp_audit/tests/test_audit_backend.py @@ -184,7 +184,7 @@ def setUpClass(cls): def test_is_post_to_thread_default_false(self): """Test that is_post_to_thread defaults to False.""" - model = self.env["ir.model"].search([("model", "=", "spp.program")], limit=1) + model = self.env["ir.model"].search([("model", "=", "res.users")], limit=1) # Delete any existing rule for this model to avoid uniqueness constraint existing_rule = self.env["spp.audit.rule"].search([("model_id", "=", model.id)], limit=1) if existing_rule: @@ -249,7 +249,7 @@ def test_rule_creation_logged(self): "OPENSPP_AUDIT_FILE_PATH": self.temp_dir, }, ): - model = self.env["ir.model"].search([("model", "=", "spp.cycle")], limit=1) + model = self.env["ir.model"].search([("model", "=", "res.users")], limit=1) # Delete any existing rule for this model to avoid uniqueness constraint existing_rule = self.env["spp.audit.rule"].search([("model_id", "=", model.id)], limit=1) if existing_rule: @@ -289,7 +289,7 @@ def test_rule_deletion_logged(self): "OPENSPP_AUDIT_FILE_PATH": self.temp_dir, }, ): - model = self.env["ir.model"].search([("model", "=", "spp.cycle")], limit=1) + model = self.env["ir.model"].search([("model", "=", "res.users")], limit=1) # Delete any existing rule for this model to avoid uniqueness constraint existing_rule = self.env["spp.audit.rule"].search([("model_id", "=", model.id)], limit=1) if existing_rule: diff --git a/spp_audit_programs/README.rst b/spp_audit_programs/README.rst new file mode 100644 index 000000000..ffc72d6e8 --- /dev/null +++ b/spp_audit_programs/README.rst @@ -0,0 +1,91 @@ +======================== +OpenSPP Audit - Programs +======================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:3d8ce2b5ae598962e2e52452909f0ed639414e808c903dbbef7f8a984981496a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OpenSPP%2FOpenSPP2-lightgray.png?logo=github + :target: https://github.com/OpenSPP/OpenSPP2/tree/19.0/spp_audit_programs + :alt: OpenSPP/OpenSPP2 + +|badge1| |badge2| |badge3| + +Ships the audit rules for program and cycle models. Split out of +``spp_audit`` so the base audit module no longer depends on +``spp_programs`` — modules that only need registry/service-point +auditing (and everything that depends on ``spp_audit``, e.g. +``spp_studio``) no longer pull in the Programs stack. + +Auto-installs whenever both ``spp_audit`` and ``spp_programs`` are +present, so program auditing is unchanged where programs are used. + +Dependencies +~~~~~~~~~~~~ + +``spp_audit``, ``spp_programs`` + +**Table of contents** + +.. contents:: + :local: + +Changelog +========= + +19.0.1.0.0 +~~~~~~~~~~ + +- Split the program/cycle audit rules out of ``spp_audit`` into this + companion (#1085), so ``spp_audit`` no longer depends on + ``spp_programs``. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* OpenSPP.org + +Maintainers +----------- + +.. |maintainer-jeremi| image:: https://github.com/jeremi.png?size=40px + :target: https://github.com/jeremi + :alt: jeremi +.. |maintainer-gonzalesedwin1123| image:: https://github.com/gonzalesedwin1123.png?size=40px + :target: https://github.com/gonzalesedwin1123 + :alt: gonzalesedwin1123 +.. |maintainer-emjay0921| image:: https://github.com/emjay0921.png?size=40px + :target: https://github.com/emjay0921 + :alt: emjay0921 + +Current maintainers: + +|maintainer-jeremi| |maintainer-gonzalesedwin1123| |maintainer-emjay0921| + +This module is part of the `OpenSPP/OpenSPP2 `_ project on GitHub. + +You are welcome to contribute. \ No newline at end of file diff --git a/spp_audit_programs/__init__.py b/spp_audit_programs/__init__.py new file mode 100644 index 000000000..441611e10 --- /dev/null +++ b/spp_audit_programs/__init__.py @@ -0,0 +1 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. diff --git a/spp_audit_programs/__manifest__.py b/spp_audit_programs/__manifest__.py new file mode 100644 index 000000000..9d5ac4fb2 --- /dev/null +++ b/spp_audit_programs/__manifest__.py @@ -0,0 +1,26 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +{ # pylint: disable=pointless-statement + "name": "OpenSPP Audit - Programs", + "category": "OpenSPP/Core", + "version": "19.0.1.0.0", + "sequence": 1, + "author": "OpenSPP.org", + "website": "https://github.com/OpenSPP/OpenSPP2", + "license": "LGPL-3", + "development_status": "Production/Stable", + "maintainers": ["jeremi", "gonzalesedwin1123", "emjay0921"], + "summary": "Audit rules for program and cycle models.", + "depends": [ + "spp_audit", + "spp_programs", + ], + "data": [ + "data/audit_rule_data.xml", + ], + "assets": {}, + "demo": [], + "images": [], + "application": False, + "installable": True, + "auto_install": ["spp_audit", "spp_programs"], +} diff --git a/spp_audit_programs/data/audit_rule_data.xml b/spp_audit_programs/data/audit_rule_data.xml new file mode 100644 index 000000000..5d7eadcde --- /dev/null +++ b/spp_audit_programs/data/audit_rule_data.xml @@ -0,0 +1,125 @@ + + + + + + + Program Rule + spp.program + + + + + + Cycle Rule + spp.cycle + + + + + + Default Eligibility Manager Rule + spp.program.membership.manager.default + + + Program Rule + program_id + + + + + Deduplication Manager Rule + spp.deduplication.manager.default + + + Program Rule + program_id + + + + + Notification Manager Rule + spp.program.notification.manager.sms + + + Program Rule + program_id + + + + + Program Manager Rule + spp.program.manager.default + + + Program Rule + program_id + + + + + Cycle Manager Rule + spp.cycle.manager.default + + + Program Rule + program_id + + + + + Entitlement Manager Rule + spp.program.entitlement.manager.default + + + Program Rule + program_id + + + + + Payment Manager Rule + spp.program.payment.manager.default + + + Program Rule + program_id + + + + + Cash Entitlement Manager Rule + spp.program.entitlement.manager.cash + + + Program Rule + program_id + + + + + Basket Entitlement Manager Rule + spp.program.entitlement.manager.basket + + + Program Rule + program_id + + diff --git a/spp_audit_programs/pyproject.toml b/spp_audit_programs/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/spp_audit_programs/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/spp_audit_programs/readme/DESCRIPTION.md b/spp_audit_programs/readme/DESCRIPTION.md new file mode 100644 index 000000000..8a08292a3 --- /dev/null +++ b/spp_audit_programs/readme/DESCRIPTION.md @@ -0,0 +1,7 @@ +Ships the audit rules for program and cycle models. Split out of `spp_audit` so the base audit module no longer depends on `spp_programs` — modules that only need registry/service-point auditing (and everything that depends on `spp_audit`, e.g. `spp_studio`) no longer pull in the Programs stack. + +Auto-installs whenever both `spp_audit` and `spp_programs` are present, so program auditing is unchanged where programs are used. + +### Dependencies + +`spp_audit`, `spp_programs` diff --git a/spp_audit_programs/readme/HISTORY.md b/spp_audit_programs/readme/HISTORY.md new file mode 100644 index 000000000..9301ca773 --- /dev/null +++ b/spp_audit_programs/readme/HISTORY.md @@ -0,0 +1,3 @@ +### 19.0.1.0.0 + +- Split the program/cycle audit rules out of `spp_audit` into this companion (#1085), so `spp_audit` no longer depends on `spp_programs`. diff --git a/spp_audit_programs/static/description/icon.png b/spp_audit_programs/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b45bba65cc8ec6fd9151ad367b26781ae1851510 GIT binary patch literal 13981 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 + + + + +OpenSPP Audit - Programs + + + +
+

OpenSPP Audit - Programs

+ + +

Production/Stable License: LGPL-3 OpenSPP/OpenSPP2

+

Ships the audit rules for program and cycle models. Split out of +spp_audit so the base audit module no longer depends on +spp_programs — modules that only need registry/service-point +auditing (and everything that depends on spp_audit, e.g. +spp_studio) no longer pull in the Programs stack.

+

Auto-installs whenever both spp_audit and spp_programs are +present, so program auditing is unchanged where programs are used.

+
+

Dependencies

+

spp_audit, spp_programs

+

Table of contents

+
+ +
+ +
+
+

19.0.1.0.0

+
    +
  • Split the program/cycle audit rules out of spp_audit into this +companion (#1085), so spp_audit no longer depends on +spp_programs.
  • +
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • OpenSPP.org
  • +
+
+
+

Maintainers

+

Current maintainers:

+

jeremi gonzalesedwin1123 emjay0921

+

This module is part of the OpenSPP/OpenSPP2 project on GitHub.

+

You are welcome to contribute.

+
+
+
+
+ + diff --git a/spp_audit_programs/tests/__init__.py b/spp_audit_programs/tests/__init__.py new file mode 100644 index 000000000..95a1d07ec --- /dev/null +++ b/spp_audit_programs/tests/__init__.py @@ -0,0 +1,3 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from . import test_program_audit_rules diff --git a/spp_audit_programs/tests/test_program_audit_rules.py b/spp_audit_programs/tests/test_program_audit_rules.py new file mode 100644 index 000000000..5edbe2c46 --- /dev/null +++ b/spp_audit_programs/tests/test_program_audit_rules.py @@ -0,0 +1,13 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Program/cycle audit rules are created by this companion (OP#1085).""" + +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestProgramAuditRules(TransactionCase): + def test_program_audit_rules_created(self): + """The Program and Cycle audit rules exist once this module is installed.""" + Rule = self.env["spp.audit.rule"] + self.assertTrue(Rule.search([("name", "=", "Program Rule")], limit=1)) + self.assertTrue(Rule.search([("name", "=", "Cycle Rule")], limit=1)) From 2fe8522e141c81af0ab6398b8362010cf0217f46 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Tue, 9 Jun 2026 15:20:29 +0800 Subject: [PATCH 5/8] fix(spp_studio): keep studio installable without spp_programs (#1083) The #1083 decoupling exposed two latent couplings that crashed when spp_studio is installed without spp_programs (or when spp_programs is installed afterwards): 1. program_ids view references: spp_studio_change_requests and spp_studio_events displayed the shared spp.studio.mixin program_ids field, which now lives in the spp_studio_programs companion. Their base views failed to load without it. Dropped program_ids from both base views and added two auto-install companions (spp_studio_change_requests_programs, spp_studio_events_programs) that re-inject the field/page when programs are present. 2. spp.cel.expression.state selection: spp_studio replaced the shared selection, dropping the base 'active'/'inactive' values. This only worked when spp_programs loaded first (forced by the old dependency). Switched to selection_add so the field is the union of both state sets, making it load-order independent. spp_programs eligibility templates (state='active') now load regardless of install order. Also guarded TestDeferredResolutionProgramOverrides to skip when spp_programs is not installed (it accessed spp.cel.program.parameter in setUpClass). --- spp_studio/models/logic.py | 25 +- spp_studio/tests/test_deferred_resolution.py | 7 + .../views/studio_cr_type_views.xml | 7 - .../README.rst | 98 ++++ .../__init__.py | 1 + .../__manifest__.py | 26 ++ .../pyproject.toml | 3 + .../readme/DESCRIPTION.md | 11 + .../readme/HISTORY.md | 3 + .../static/description/icon.png | Bin 0 -> 15480 bytes .../static/description/index.html | 435 ++++++++++++++++++ .../tests/__init__.py | 2 + .../tests/test_cr_type_programs_view.py | 16 + .../views/studio_cr_type_views.xml | 23 + .../views/studio_event_type_views.xml | 17 - spp_studio_events_programs/README.rst | 97 ++++ spp_studio_events_programs/__init__.py | 1 + spp_studio_events_programs/__manifest__.py | 26 ++ spp_studio_events_programs/pyproject.toml | 3 + .../readme/DESCRIPTION.md | 11 + spp_studio_events_programs/readme/HISTORY.md | 3 + .../static/description/icon.png | Bin 0 -> 15480 bytes .../static/description/index.html | 434 +++++++++++++++++ spp_studio_events_programs/tests/__init__.py | 2 + .../tests/test_event_type_programs_view.py | 16 + .../views/studio_event_type_views.xml | 30 ++ 26 files changed, 1265 insertions(+), 32 deletions(-) create mode 100644 spp_studio_change_requests_programs/README.rst create mode 100644 spp_studio_change_requests_programs/__init__.py create mode 100644 spp_studio_change_requests_programs/__manifest__.py create mode 100644 spp_studio_change_requests_programs/pyproject.toml create mode 100644 spp_studio_change_requests_programs/readme/DESCRIPTION.md create mode 100644 spp_studio_change_requests_programs/readme/HISTORY.md create mode 100644 spp_studio_change_requests_programs/static/description/icon.png create mode 100644 spp_studio_change_requests_programs/static/description/index.html create mode 100644 spp_studio_change_requests_programs/tests/__init__.py create mode 100644 spp_studio_change_requests_programs/tests/test_cr_type_programs_view.py create mode 100644 spp_studio_change_requests_programs/views/studio_cr_type_views.xml create mode 100644 spp_studio_events_programs/README.rst create mode 100644 spp_studio_events_programs/__init__.py create mode 100644 spp_studio_events_programs/__manifest__.py create mode 100644 spp_studio_events_programs/pyproject.toml create mode 100644 spp_studio_events_programs/readme/DESCRIPTION.md create mode 100644 spp_studio_events_programs/readme/HISTORY.md create mode 100644 spp_studio_events_programs/static/description/icon.png create mode 100644 spp_studio_events_programs/static/description/index.html create mode 100644 spp_studio_events_programs/tests/__init__.py create mode 100644 spp_studio_events_programs/tests/test_event_type_programs_view.py create mode 100644 spp_studio_events_programs/views/studio_event_type_views.xml diff --git a/spp_studio/models/logic.py b/spp_studio/models/logic.py index a6356622d..da40d358f 100644 --- a/spp_studio/models/logic.py +++ b/spp_studio/models/logic.py @@ -152,19 +152,28 @@ def write(self, vals): # LIFECYCLE (override state from parent for Studio governance workflow) # ═══════════════════════════════════════════════════════════════════════ - # Override state field with Studio-specific values - # Parent has: draft, active, inactive - # Studio needs: draft, pending, published, archived + # Extend the shared spp.cel.expression.state with Studio's governance + # states. Parent (spp_cel_domain) has: draft, active, inactive. + # Studio adds: pending, published, archived. + # + # Use selection_add (NOT a full selection= override) so the base + # lifecycle values are preserved. Replacing the selection drops "active" + # /"inactive", which then breaks dependent modules whose data relies on + # them (e.g. spp_programs eligibility templates created with + # state="active") depending on module load order. selection_add unions + # the two sets, so the result is load-order independent. Studio's own + # views restrict the visible workflow states via statusbar_visible. state = fields.Selection( - selection=[ - ("draft", "Draft"), + selection_add=[ ("pending", "Pending Approval"), ("published", "Published"), ("archived", "Archived"), ], - string="Status", - default="draft", - required=True, + ondelete={ + "pending": "set default", + "published": "set default", + "archived": "set default", + }, tracking=True, help="Current lifecycle state of the logic (Studio governance workflow)", ) diff --git a/spp_studio/tests/test_deferred_resolution.py b/spp_studio/tests/test_deferred_resolution.py index babde4c54..13e16ca92 100644 --- a/spp_studio/tests/test_deferred_resolution.py +++ b/spp_studio/tests/test_deferred_resolution.py @@ -9,6 +9,7 @@ """ import logging +import unittest from odoo.tests import TransactionCase, tagged @@ -486,6 +487,12 @@ class TestDeferredResolutionProgramOverrides(TransactionCase, CELTestDataMixin): def setUpClass(cls): """Set up test data.""" super().setUpClass() + # Program-specific overrides require spp_programs (the + # spp.cel.program.parameter model lives there, surfaced via + # spp_studio_programs). When spp_studio is installed without programs, + # skip the whole class instead of erroring in setUpClass. + if "spp.cel.program.parameter" not in cls.env: + raise unittest.SkipTest("spp.cel.program.parameter not available (spp_programs not installed)") cls._test_id = cls._get_unique_test_id() cls.LogicVariable = cls.env["spp.cel.variable"] cls.LogicVariableResolver = cls.env["spp.cel.variable.resolver"] diff --git a/spp_studio_change_requests/views/studio_cr_type_views.xml b/spp_studio_change_requests/views/studio_cr_type_views.xml index 2600fb555..603d7187d 100644 --- a/spp_studio_change_requests/views/studio_cr_type_views.xml +++ b/spp_studio_change_requests/views/studio_cr_type_views.xml @@ -258,13 +258,6 @@ /> - - - diff --git a/spp_studio_change_requests_programs/README.rst b/spp_studio_change_requests_programs/README.rst new file mode 100644 index 000000000..7792c52e1 --- /dev/null +++ b/spp_studio_change_requests_programs/README.rst @@ -0,0 +1,98 @@ +========================================= +OpenSPP Studio Change Requests - Programs +========================================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:f584665a9d5994d41a99e015339eab8d20335ea5d909cb5efd1ea7fb173aa221 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OpenSPP%2FOpenSPP2-lightgray.png?logo=github + :target: https://github.com/OpenSPP/OpenSPP2/tree/19.0/spp_studio_change_requests_programs + :alt: OpenSPP/OpenSPP2 + +|badge1| |badge2| |badge3| + +Re-adds program scoping to Studio change request types. Split out of +``spp_studio_change_requests`` so the base module no longer depends +(transitively) on ``spp_programs`` — deployments without programs can +use Studio change requests without installing the Programs stack. + +Auto-installs whenever both ``spp_studio_change_requests`` and +``spp_studio_programs`` are present, so the program-scoping field +reappears on the change request type form exactly where programs are +used. + +Key Capabilities +~~~~~~~~~~~~~~~~ + +- Injects the ``program_ids`` field back into the change request type + form (Technical Info → Programs) + +Dependencies +~~~~~~~~~~~~ + +``spp_studio_change_requests``, ``spp_studio_programs`` + +**Table of contents** + +.. contents:: + :local: + +Changelog +========= + +19.0.1.0.0 +~~~~~~~~~~ + +- Split program scoping (the change request type's ``program_ids`` + field) out of ``spp_studio_change_requests`` into this companion + (#1083), so the base module loads without ``spp_programs``. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* OpenSPP.org + +Maintainers +----------- + +.. |maintainer-jeremi| image:: https://github.com/jeremi.png?size=40px + :target: https://github.com/jeremi + :alt: jeremi +.. |maintainer-gonzalesedwin1123| image:: https://github.com/gonzalesedwin1123.png?size=40px + :target: https://github.com/gonzalesedwin1123 + :alt: gonzalesedwin1123 +.. |maintainer-emjay0921| image:: https://github.com/emjay0921.png?size=40px + :target: https://github.com/emjay0921 + :alt: emjay0921 + +Current maintainers: + +|maintainer-jeremi| |maintainer-gonzalesedwin1123| |maintainer-emjay0921| + +This module is part of the `OpenSPP/OpenSPP2 `_ project on GitHub. + +You are welcome to contribute. \ No newline at end of file diff --git a/spp_studio_change_requests_programs/__init__.py b/spp_studio_change_requests_programs/__init__.py new file mode 100644 index 000000000..441611e10 --- /dev/null +++ b/spp_studio_change_requests_programs/__init__.py @@ -0,0 +1 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. diff --git a/spp_studio_change_requests_programs/__manifest__.py b/spp_studio_change_requests_programs/__manifest__.py new file mode 100644 index 000000000..b59c95ad4 --- /dev/null +++ b/spp_studio_change_requests_programs/__manifest__.py @@ -0,0 +1,26 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +{ # pylint: disable=pointless-statement + "name": "OpenSPP Studio Change Requests - Programs", + "category": "OpenSPP/Configuration", + "version": "19.0.1.0.0", + "sequence": 1, + "author": "OpenSPP.org", + "website": "https://github.com/OpenSPP/OpenSPP2", + "license": "LGPL-3", + "development_status": "Production/Stable", + "maintainers": ["jeremi", "gonzalesedwin1123", "emjay0921"], + "summary": "Program scoping for Studio change request types.", + "depends": [ + "spp_studio_change_requests", + "spp_studio_programs", + ], + "data": [ + "views/studio_cr_type_views.xml", + ], + "assets": {}, + "demo": [], + "images": [], + "application": False, + "installable": True, + "auto_install": ["spp_studio_change_requests", "spp_studio_programs"], +} diff --git a/spp_studio_change_requests_programs/pyproject.toml b/spp_studio_change_requests_programs/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/spp_studio_change_requests_programs/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/spp_studio_change_requests_programs/readme/DESCRIPTION.md b/spp_studio_change_requests_programs/readme/DESCRIPTION.md new file mode 100644 index 000000000..0c02f91d3 --- /dev/null +++ b/spp_studio_change_requests_programs/readme/DESCRIPTION.md @@ -0,0 +1,11 @@ +Re-adds program scoping to Studio change request types. Split out of `spp_studio_change_requests` so the base module no longer depends (transitively) on `spp_programs` — deployments without programs can use Studio change requests without installing the Programs stack. + +Auto-installs whenever both `spp_studio_change_requests` and `spp_studio_programs` are present, so the program-scoping field reappears on the change request type form exactly where programs are used. + +### Key Capabilities + +- Injects the `program_ids` field back into the change request type form (Technical Info → Programs) + +### Dependencies + +`spp_studio_change_requests`, `spp_studio_programs` diff --git a/spp_studio_change_requests_programs/readme/HISTORY.md b/spp_studio_change_requests_programs/readme/HISTORY.md new file mode 100644 index 000000000..89f59abef --- /dev/null +++ b/spp_studio_change_requests_programs/readme/HISTORY.md @@ -0,0 +1,3 @@ +### 19.0.1.0.0 + +- Split program scoping (the change request type's ``program_ids`` field) out of `spp_studio_change_requests` into this companion (#1083), so the base module loads without `spp_programs`. diff --git a/spp_studio_change_requests_programs/static/description/icon.png b/spp_studio_change_requests_programs/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c7dbdaaf1dace8f0ccf8c2087047ddfcf584af0c GIT binary patch literal 15480 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 + + + + +OpenSPP Studio Change Requests - Programs + + + +
+

OpenSPP Studio Change Requests - Programs

+ + +

Production/Stable License: LGPL-3 OpenSPP/OpenSPP2

+

Re-adds program scoping to Studio change request types. Split out of +spp_studio_change_requests so the base module no longer depends +(transitively) on spp_programs — deployments without programs can +use Studio change requests without installing the Programs stack.

+

Auto-installs whenever both spp_studio_change_requests and +spp_studio_programs are present, so the program-scoping field +reappears on the change request type form exactly where programs are +used.

+
+

Key Capabilities

+
    +
  • Injects the program_ids field back into the change request type +form (Technical Info → Programs)
  • +
+
+
+

Dependencies

+

spp_studio_change_requests, spp_studio_programs

+

Table of contents

+
+ +
+ +
+
+

19.0.1.0.0

+
    +
  • Split program scoping (the change request type’s program_ids +field) out of spp_studio_change_requests into this companion +(#1083), so the base module loads without spp_programs.
  • +
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • OpenSPP.org
  • +
+
+
+

Maintainers

+

Current maintainers:

+

jeremi gonzalesedwin1123 emjay0921

+

This module is part of the OpenSPP/OpenSPP2 project on GitHub.

+

You are welcome to contribute.

+
+
+
+
+ + diff --git a/spp_studio_change_requests_programs/tests/__init__.py b/spp_studio_change_requests_programs/tests/__init__.py new file mode 100644 index 000000000..98fb16733 --- /dev/null +++ b/spp_studio_change_requests_programs/tests/__init__.py @@ -0,0 +1,2 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +from . import test_cr_type_programs_view diff --git a/spp_studio_change_requests_programs/tests/test_cr_type_programs_view.py b/spp_studio_change_requests_programs/tests/test_cr_type_programs_view.py new file mode 100644 index 000000000..18f143d63 --- /dev/null +++ b/spp_studio_change_requests_programs/tests/test_cr_type_programs_view.py @@ -0,0 +1,16 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""This companion re-adds program_ids to the change request type form.""" + +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestCrTypeProgramsView(TransactionCase): + def test_program_ids_in_form_view(self): + """The change request type form exposes program_ids when installed.""" + CrType = self.env["spp.studio.change.request.type"] + # Field is provided by spp_studio_programs (a dependency) + self.assertIn("program_ids", CrType._fields) + # ...and this companion injects it back into the form arch + arch = CrType.get_view(view_type="form")["arch"] + self.assertIn("program_ids", arch) diff --git a/spp_studio_change_requests_programs/views/studio_cr_type_views.xml b/spp_studio_change_requests_programs/views/studio_cr_type_views.xml new file mode 100644 index 000000000..599e0b587 --- /dev/null +++ b/spp_studio_change_requests_programs/views/studio_cr_type_views.xml @@ -0,0 +1,23 @@ + + + + + spp.studio.change.request.type.form.programs + spp.studio.change.request.type + + + + + + + + + + diff --git a/spp_studio_events/views/studio_event_type_views.xml b/spp_studio_events/views/studio_event_type_views.xml index ef35b6ca9..57788fc3e 100644 --- a/spp_studio_events/views/studio_event_type_views.xml +++ b/spp_studio_events/views/studio_event_type_views.xml @@ -395,23 +395,6 @@
- - - - - - - - - diff --git a/spp_studio_events_programs/README.rst b/spp_studio_events_programs/README.rst new file mode 100644 index 000000000..bebae9ab4 --- /dev/null +++ b/spp_studio_events_programs/README.rst @@ -0,0 +1,97 @@ +================================ +OpenSPP Studio Events - Programs +================================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:e9883cde3c90d81d3ab0ab8cd5bdad09e45118ef076af07c7802f901e390c0e9 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OpenSPP%2FOpenSPP2-lightgray.png?logo=github + :target: https://github.com/OpenSPP/OpenSPP2/tree/19.0/spp_studio_events_programs + :alt: OpenSPP/OpenSPP2 + +|badge1| |badge2| |badge3| + +Re-adds program scoping to Studio event types. Split out of +``spp_studio_events`` so the base module no longer depends +(transitively) on ``spp_programs`` — deployments without programs can +use Studio events without installing the Programs stack. + +Auto-installs whenever both ``spp_studio_events`` and +``spp_studio_programs`` are present, so the program-scoping page +reappears on the event type form exactly where programs are used. + +Key Capabilities +~~~~~~~~~~~~~~~~ + +- Injects the Programs page (``program_ids``) back into the event type + form + +Dependencies +~~~~~~~~~~~~ + +``spp_studio_events``, ``spp_studio_programs`` + +**Table of contents** + +.. contents:: + :local: + +Changelog +========= + +19.0.1.0.0 +~~~~~~~~~~ + +- Split program scoping (the event type's ``program_ids`` page) out of + ``spp_studio_events`` into this companion (#1083), so the base module + loads without ``spp_programs``. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* OpenSPP.org + +Maintainers +----------- + +.. |maintainer-jeremi| image:: https://github.com/jeremi.png?size=40px + :target: https://github.com/jeremi + :alt: jeremi +.. |maintainer-gonzalesedwin1123| image:: https://github.com/gonzalesedwin1123.png?size=40px + :target: https://github.com/gonzalesedwin1123 + :alt: gonzalesedwin1123 +.. |maintainer-emjay0921| image:: https://github.com/emjay0921.png?size=40px + :target: https://github.com/emjay0921 + :alt: emjay0921 + +Current maintainers: + +|maintainer-jeremi| |maintainer-gonzalesedwin1123| |maintainer-emjay0921| + +This module is part of the `OpenSPP/OpenSPP2 `_ project on GitHub. + +You are welcome to contribute. \ No newline at end of file diff --git a/spp_studio_events_programs/__init__.py b/spp_studio_events_programs/__init__.py new file mode 100644 index 000000000..441611e10 --- /dev/null +++ b/spp_studio_events_programs/__init__.py @@ -0,0 +1 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. diff --git a/spp_studio_events_programs/__manifest__.py b/spp_studio_events_programs/__manifest__.py new file mode 100644 index 000000000..59ffa889e --- /dev/null +++ b/spp_studio_events_programs/__manifest__.py @@ -0,0 +1,26 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +{ # pylint: disable=pointless-statement + "name": "OpenSPP Studio Events - Programs", + "category": "OpenSPP/Configuration", + "version": "19.0.1.0.0", + "sequence": 1, + "author": "OpenSPP.org", + "website": "https://github.com/OpenSPP/OpenSPP2", + "license": "LGPL-3", + "development_status": "Production/Stable", + "maintainers": ["jeremi", "gonzalesedwin1123", "emjay0921"], + "summary": "Program scoping for Studio event types.", + "depends": [ + "spp_studio_events", + "spp_studio_programs", + ], + "data": [ + "views/studio_event_type_views.xml", + ], + "assets": {}, + "demo": [], + "images": [], + "application": False, + "installable": True, + "auto_install": ["spp_studio_events", "spp_studio_programs"], +} diff --git a/spp_studio_events_programs/pyproject.toml b/spp_studio_events_programs/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/spp_studio_events_programs/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/spp_studio_events_programs/readme/DESCRIPTION.md b/spp_studio_events_programs/readme/DESCRIPTION.md new file mode 100644 index 000000000..8290968a7 --- /dev/null +++ b/spp_studio_events_programs/readme/DESCRIPTION.md @@ -0,0 +1,11 @@ +Re-adds program scoping to Studio event types. Split out of `spp_studio_events` so the base module no longer depends (transitively) on `spp_programs` — deployments without programs can use Studio events without installing the Programs stack. + +Auto-installs whenever both `spp_studio_events` and `spp_studio_programs` are present, so the program-scoping page reappears on the event type form exactly where programs are used. + +### Key Capabilities + +- Injects the Programs page (`program_ids`) back into the event type form + +### Dependencies + +`spp_studio_events`, `spp_studio_programs` diff --git a/spp_studio_events_programs/readme/HISTORY.md b/spp_studio_events_programs/readme/HISTORY.md new file mode 100644 index 000000000..f5426c314 --- /dev/null +++ b/spp_studio_events_programs/readme/HISTORY.md @@ -0,0 +1,3 @@ +### 19.0.1.0.0 + +- Split program scoping (the event type's ``program_ids`` page) out of `spp_studio_events` into this companion (#1083), so the base module loads without `spp_programs`. diff --git a/spp_studio_events_programs/static/description/icon.png b/spp_studio_events_programs/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c7dbdaaf1dace8f0ccf8c2087047ddfcf584af0c GIT binary patch literal 15480 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 + + + + +OpenSPP Studio Events - Programs + + + +
+

OpenSPP Studio Events - Programs

+ + +

Production/Stable License: LGPL-3 OpenSPP/OpenSPP2

+

Re-adds program scoping to Studio event types. Split out of +spp_studio_events so the base module no longer depends +(transitively) on spp_programs — deployments without programs can +use Studio events without installing the Programs stack.

+

Auto-installs whenever both spp_studio_events and +spp_studio_programs are present, so the program-scoping page +reappears on the event type form exactly where programs are used.

+
+

Key Capabilities

+
    +
  • Injects the Programs page (program_ids) back into the event type +form
  • +
+
+
+

Dependencies

+

spp_studio_events, spp_studio_programs

+

Table of contents

+
+ +
+ +
+
+

19.0.1.0.0

+
    +
  • Split program scoping (the event type’s program_ids page) out of +spp_studio_events into this companion (#1083), so the base module +loads without spp_programs.
  • +
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • OpenSPP.org
  • +
+
+
+

Maintainers

+

Current maintainers:

+

jeremi gonzalesedwin1123 emjay0921

+

This module is part of the OpenSPP/OpenSPP2 project on GitHub.

+

You are welcome to contribute.

+
+
+
+
+ + diff --git a/spp_studio_events_programs/tests/__init__.py b/spp_studio_events_programs/tests/__init__.py new file mode 100644 index 000000000..4b4a9681b --- /dev/null +++ b/spp_studio_events_programs/tests/__init__.py @@ -0,0 +1,2 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +from . import test_event_type_programs_view diff --git a/spp_studio_events_programs/tests/test_event_type_programs_view.py b/spp_studio_events_programs/tests/test_event_type_programs_view.py new file mode 100644 index 000000000..d54e89376 --- /dev/null +++ b/spp_studio_events_programs/tests/test_event_type_programs_view.py @@ -0,0 +1,16 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""This companion re-adds program_ids to the event type form.""" + +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestEventTypeProgramsView(TransactionCase): + def test_program_ids_in_form_view(self): + """The event type form exposes program_ids when installed.""" + EventType = self.env["spp.studio.event.type"] + # Field is provided by spp_studio_programs (a dependency) + self.assertIn("program_ids", EventType._fields) + # ...and this companion injects it back into the form arch + arch = EventType.get_view(view_type="form")["arch"] + self.assertIn("program_ids", arch) diff --git a/spp_studio_events_programs/views/studio_event_type_views.xml b/spp_studio_events_programs/views/studio_event_type_views.xml new file mode 100644 index 000000000..3216af93f --- /dev/null +++ b/spp_studio_events_programs/views/studio_event_type_views.xml @@ -0,0 +1,30 @@ + + + + + spp.studio.event.type.form.programs + spp.studio.event.type + + + + + + + + + + + + + + + + From 5aa080852d4a78e4b6045369753828ef82cebfdf Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Tue, 9 Jun 2026 15:43:54 +0800 Subject: [PATCH 6/8] test(spp_api_v2): move program-membership scope isolation test to companion (#1081) CI runs each module's tests in isolation. TestScopeIsolation.setUp in spp_api_v2 created a program + membership for all its tests, which fails with KeyError: 'spp.program' when spp_api_v2 is tested without spp_programs. Only one test (program-membership scope isolation) needs programs, and it targets the /ProgramMembership endpoint that moved to spp_api_v2_programs in #1081. - Trim TestScopeIsolation.setUp to the resource types the base module serves (individual, group); the other 6 isolation tests need no program - Move test_individual_scope_does_not_grant_program_membership_access to spp_api_v2_programs (TestScopeEnforcementProgramMembership) Also apply pre-commit auto-format fixes that drifted on files from earlier commits in this branch (import order, line length, blank lines). --- spp_api_v2/tests/test_scope_enforcement.py | 45 ++----------------- .../tests/test_program_service.py | 3 +- .../tests/test_scope_enforcement_program.py | 36 +++++++++++++++ .../migrations/1.0/post-migrate.py | 4 +- spp_studio/models/studio_mixin.py | 1 - spp_studio/tests/test_studio_mixin.py | 1 - 6 files changed, 42 insertions(+), 48 deletions(-) diff --git a/spp_api_v2/tests/test_scope_enforcement.py b/spp_api_v2/tests/test_scope_enforcement.py index df67b3122..34da39e1a 100644 --- a/spp_api_v2/tests/test_scope_enforcement.py +++ b/spp_api_v2/tests/test_scope_enforcement.py @@ -492,14 +492,11 @@ class TestScopeIsolation(ApiV2HttpTestCase): def setUp(self): super().setUp() - # Create test data for all resource types + # Create test data for the resource types served by the base module. + # Program / program-membership isolation lives in spp_api_v2_programs + # (the program endpoints moved there in OP#1081). self.individual = self.create_test_individual(identifier_value="ISOLATION-IND-001") self.group = self.create_test_group(identifier_value="ISOLATION-GRP-001") - self.program = self.create_test_program(name="Isolation Test Program") - self.membership = self.create_test_membership( - partner=self.individual, - program=self.program, - ) def test_individual_scope_does_not_grant_group_access(self): """individual:read scope does not grant access to group endpoints""" @@ -539,42 +536,6 @@ def test_group_scope_does_not_grant_individual_access(self): self.assertIn("Missing required scope", data["detail"]) self.assertIn("individual:read", data["detail"]) - def test_individual_scope_does_not_grant_program_membership_access(self): - """individual:create scope does not grant access to program_membership endpoints""" - # Create client with only individual:create scope - client = self.create_api_client( - name="Individual Create Only Client", - scopes=[{"resource": "individual", "action": "create"}], - require_consent=False, - ) - token = self.generate_jwt_token(client) - - # Try to create program membership - payload = { - "resourceType": "ProgramMembership", - "status": "enrolled", - "beneficiary": { - "reference": "Individual/urn:openspp:vocab:id-type#test_national_id|ISOLATION-IND-001", - }, - "program": { - "reference": "Program/urn:openspp:program|isolation-test-program", - }, - } - - response = self.url_open( - "/api/v2/spp/ProgramMembership", - data=json.dumps(payload), - headers={ - "Content-Type": "application/json", - "Authorization": f"Bearer {token}", - }, - ) - - self.assertEqual(response.status_code, 403) - data = json.loads(response.content) - self.assertIn("Missing required scope", data["detail"]) - self.assertIn("program_membership:create", data["detail"]) - def test_read_scope_does_not_grant_create_access(self): """individual:read scope does not grant create access""" # Create client with only individual:read scope diff --git a/spp_api_v2_programs/tests/test_program_service.py b/spp_api_v2_programs/tests/test_program_service.py index b072c79ff..f364fae15 100644 --- a/spp_api_v2_programs/tests/test_program_service.py +++ b/spp_api_v2_programs/tests/test_program_service.py @@ -1,9 +1,10 @@ # Part of OpenSPP. See LICENSE file for full copyright and licensing details. """Tests for ProgramService""" -from ..services.program_service import ProgramService from odoo.addons.spp_api_v2.tests.common import ApiV2TestCase +from ..services.program_service import ProgramService + class TestProgramService(ApiV2TestCase): """Test ProgramService functionality""" diff --git a/spp_api_v2_programs/tests/test_scope_enforcement_program.py b/spp_api_v2_programs/tests/test_scope_enforcement_program.py index 8aff7549c..0feced987 100644 --- a/spp_api_v2_programs/tests/test_scope_enforcement_program.py +++ b/spp_api_v2_programs/tests/test_scope_enforcement_program.py @@ -102,3 +102,39 @@ def test_program_membership_update_requires_scope(self): data = json.loads(response.content) self.assertIn("Missing required scope", data["detail"]) self.assertIn("program_membership:update", data["detail"]) + + def test_individual_scope_does_not_grant_program_membership_access(self): + """individual:create scope does not grant access to program_membership endpoints""" + # Create client with only individual:create scope + client = self.create_api_client( + name="Individual Create Only Client", + scopes=[{"resource": "individual", "action": "create"}], + require_consent=False, + ) + token = self.generate_jwt_token(client) + + # Try to create program membership + payload = { + "resourceType": "ProgramMembership", + "status": "enrolled", + "beneficiary": { + "reference": "Individual/urn:openspp:vocab:id-type#test_national_id|SCOPE-PM-001", + }, + "program": { + "reference": "Program/urn:openspp:program|test-program", + }, + } + + response = self.url_open( + self.pm_url, + data=json.dumps(payload), + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {token}", + }, + ) + + self.assertEqual(response.status_code, 403) + data = json.loads(response.content) + self.assertIn("Missing required scope", data["detail"]) + self.assertIn("program_membership:create", data["detail"]) diff --git a/spp_source_tracking/migrations/1.0/post-migrate.py b/spp_source_tracking/migrations/1.0/post-migrate.py index ec0674ac2..bde5d7d8b 100644 --- a/spp_source_tracking/migrations/1.0/post-migrate.py +++ b/spp_source_tracking/migrations/1.0/post-migrate.py @@ -47,9 +47,7 @@ def migrate(cr, version): # Update existing program memberships — only when the programs stack is # installed (the source-tracking extension now lives in # spp_source_tracking_programs; the table may be absent otherwise). - cr.execute( - "SELECT 1 FROM information_schema.tables WHERE table_name = 'spp_program_membership'" - ) + cr.execute("SELECT 1 FROM information_schema.tables WHERE table_name = 'spp_program_membership'") if cr.fetchone(): cr.execute( """ diff --git a/spp_studio/models/studio_mixin.py b/spp_studio/models/studio_mixin.py index 4580ff60d..8afbc159e 100644 --- a/spp_studio/models/studio_mixin.py +++ b/spp_studio/models/studio_mixin.py @@ -78,7 +78,6 @@ class StudioMixin(models.AbstractModel): copy=False, ) - def action_activate(self): """Activate the configuration, making it available for use.""" self.ensure_one() diff --git a/spp_studio/tests/test_studio_mixin.py b/spp_studio/tests/test_studio_mixin.py index b284f3e5c..1a3cd3b28 100644 --- a/spp_studio/tests/test_studio_mixin.py +++ b/spp_studio/tests/test_studio_mixin.py @@ -41,4 +41,3 @@ def test_audit_fields_defined(self): self.assertIn("activated_date", fields) self.assertIn("deactivated_by_id", fields) self.assertIn("deactivated_date", fields) - From 8a8f16dace8d2048401a0af0f99e0d1259b3b081 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Wed, 10 Jun 2026 12:16:17 +0800 Subject: [PATCH 7/8] style(spp_api_v2_programs): sort imports in moved router/test files (#1081) Group first-party odoo.addons.spp_api_v2.* imports ahead of relative imports in the files moved from spp_api_v2 in #1081. Import-order only, no behavior change; fixes the pre-commit ruff check. --- spp_api_v2_programs/routers/program.py | 4 ++-- spp_api_v2_programs/routers/program_membership.py | 8 ++++---- .../tests/test_program_membership_service.py | 3 ++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/spp_api_v2_programs/routers/program.py b/spp_api_v2_programs/routers/program.py index e7405e9ef..98d431fb5 100644 --- a/spp_api_v2_programs/routers/program.py +++ b/spp_api_v2_programs/routers/program.py @@ -8,6 +8,8 @@ from odoo.osv import expression from odoo.addons.fastapi.dependencies import odoo_env +from odoo.addons.spp_api_v2.middleware.auth import get_authenticated_client +from odoo.addons.spp_api_v2.schemas.search_result import SearchResult, create_search_result from fastapi import ( APIRouter, @@ -20,9 +22,7 @@ status, ) -from odoo.addons.spp_api_v2.middleware.auth import get_authenticated_client from ..schemas.program import Program -from odoo.addons.spp_api_v2.schemas.search_result import SearchResult, create_search_result from ..services.program_service import ProgramService _logger = logging.getLogger(__name__) diff --git a/spp_api_v2_programs/routers/program_membership.py b/spp_api_v2_programs/routers/program_membership.py index 07022aca3..0a3b34e6c 100644 --- a/spp_api_v2_programs/routers/program_membership.py +++ b/spp_api_v2_programs/routers/program_membership.py @@ -7,6 +7,10 @@ from odoo.api import Environment from odoo.addons.fastapi.dependencies import odoo_env +from odoo.addons.spp_api_v2.middleware.auth import get_authenticated_client +from odoo.addons.spp_api_v2.schemas.search_result import SearchResult, create_search_result +from odoo.addons.spp_api_v2.services.consent_service import ConsentService +from odoo.addons.spp_api_v2.utils.pagination import fetch_with_consent from fastapi import ( APIRouter, @@ -20,12 +24,8 @@ status, ) -from odoo.addons.spp_api_v2.middleware.auth import get_authenticated_client from ..schemas.program_membership import ProgramMembership -from odoo.addons.spp_api_v2.schemas.search_result import SearchResult, create_search_result -from odoo.addons.spp_api_v2.services.consent_service import ConsentService from ..services.program_membership_service import ProgramMembershipService -from odoo.addons.spp_api_v2.utils.pagination import fetch_with_consent _logger = logging.getLogger(__name__) diff --git a/spp_api_v2_programs/tests/test_program_membership_service.py b/spp_api_v2_programs/tests/test_program_membership_service.py index efea3bb5c..a536b2f25 100644 --- a/spp_api_v2_programs/tests/test_program_membership_service.py +++ b/spp_api_v2_programs/tests/test_program_membership_service.py @@ -6,9 +6,10 @@ from odoo.exceptions import ValidationError from odoo.addons.spp_api_v2.schemas.base import Reference +from odoo.addons.spp_api_v2.tests.common import ApiV2TestCase + from ..schemas.program_membership import ProgramMembership from ..services.program_membership_service import ProgramMembershipService -from odoo.addons.spp_api_v2.tests.common import ApiV2TestCase class TestProgramMembershipService(ApiV2TestCase): From 52410675cceda126cf95275bc305c5a27a936f05 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Wed, 10 Jun 2026 15:05:48 +0800 Subject: [PATCH 8/8] test: raise coverage on spp_api_v2_programs and spp_studio_programs (#1081, #1083) The program-membership PUT update tests used url_open (which cannot issue a PUT) and only asserted [200, 405], so the update endpoint body was never exercised. Rewrite them to use url_put and add the missing paths: - update success (status change persisted) - 409 on stale If-Match, 404 on unknown membership, 400 on bad identifier - 422 create/update error paths (unknown program reference) - previous-page link on non-zero _offset Add a test for the pack-install wizard's _get_pack_program_id hook (selected program and the None branch). spp_api_v2_programs 86% -> 92%, spp_studio_programs 87% -> 93%. Test-only. --- .../tests/test_program_membership_api.py | 150 ++++++++++++++---- spp_studio_programs/tests/__init__.py | 1 + .../test_pack_install_wizard_programs.py | 33 ++++ 3 files changed, 149 insertions(+), 35 deletions(-) create mode 100644 spp_studio_programs/tests/test_pack_install_wizard_programs.py diff --git a/spp_api_v2_programs/tests/test_program_membership_api.py b/spp_api_v2_programs/tests/test_program_membership_api.py index 639eb8b72..f06c3231a 100644 --- a/spp_api_v2_programs/tests/test_program_membership_api.py +++ b/spp_api_v2_programs/tests/test_program_membership_api.py @@ -304,51 +304,52 @@ def test_create_program_membership_validation_error(self): self.assertEqual(response.status_code, 422) def test_update_program_membership_success(self): - """PUT /ProgramMembership/{id} updates membership""" + """PUT /ProgramMembership/{id} updates membership status""" url = f"{self.api_base_url}/urn:openspp:vocab:id-type%23test_national_id|ENROLL-001" - # Get current version + # Use the current representation as the update payload get_response = self.url_open(url, headers=self._get_headers()) - current_data = json.loads(get_response.content) - version_id = current_data["meta"]["versionId"] - - # Update to paused status - payload = current_data.copy() + payload = json.loads(get_response.content) payload["status"] = "paused" - headers = self._get_headers() - headers["If-Match"] = f'"{version_id}"' + response = self.url_put(url, data=json.dumps(payload), headers=self._get_headers()) - response = self.url_open( - url, - data=json.dumps(payload), - headers=headers, - ) - - # Note: PUT method needs special handling in url_open - # For now, check that endpoint exists (200/405 acceptable) - self.assertIn(response.status_code, [200, 405]) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content) + self.assertEqual(data["status"], "paused") + # Persisted on the record + self.membership.invalidate_recordset() + self.assertEqual(self.membership.state, "paused") - def test_update_program_membership_without_if_match_still_works(self): - """PUT without If-Match still works (optional optimistic locking)""" + def test_update_program_membership_wrong_if_match_returns_409(self): + """PUT with a stale If-Match version returns 409 Conflict""" url = f"{self.api_base_url}/urn:openspp:vocab:id-type%23test_national_id|ENROLL-001" - # Get current data get_response = self.url_open(url, headers=self._get_headers()) - current_data = json.loads(get_response.content) - - # Update without If-Match header - payload = current_data.copy() + payload = json.loads(get_response.content) payload["status"] = "paused" - response = self.url_open( - url, - data=json.dumps(payload), - headers=self._get_headers(), - ) + headers = self._get_headers() + headers["If-Match"] = '"stale-version-0"' + + response = self.url_put(url, data=json.dumps(payload), headers=headers) + self.assertEqual(response.status_code, 409) - # Should still work (200/405 acceptable in test environment) - self.assertIn(response.status_code, [200, 405]) + def test_update_program_membership_not_found_returns_404(self): + """PUT to a non-existent membership returns 404""" + url = f"{self.api_base_url}/urn:openspp:vocab:id-type%23test_national_id|NONEXISTENT-PUT" + + payload = { + "type": "ProgramMembership", + "program": {"reference": "Program/urn:openspp:program|test-enrollment-program"}, + "beneficiary": { + "reference": "Individual/urn:openspp:vocab:id-type%23test_national_id|NONEXISTENT-PUT", + }, + "status": "paused", + } + + response = self.url_put(url, data=json.dumps(payload), headers=self._get_headers()) + self.assertEqual(response.status_code, 404) def test_update_program_membership_no_scope(self): """PUT without update scope returns 403""" @@ -373,14 +374,93 @@ def test_update_program_membership_no_scope(self): "status": "paused", } - response = self.url_open( + response = self.url_put( url, data=json.dumps(payload), headers=self._get_headers(token=read_only_token), ) - # 403 or 405 (method not allowed in test mode) - self.assertIn(response.status_code, [403, 405]) + self.assertEqual(response.status_code, 403) + + def test_create_program_membership_unknown_program_returns_422(self): + """POST referencing a non-existent program hits the create error path (422)""" + new_individual = self.create_test_individual(identifier_value="ERR-ENROLL-001") + self.create_consent( + registrant=new_individual, + grantee_partner=self.client.partner_id, + resource_type="all", + field_access="all", + ) + + payload = { + "type": "ProgramMembership", + "program": {"reference": "Program/urn:openspp:program|does-not-exist-program"}, + "beneficiary": { + "reference": "Individual/urn:openspp:vocab:id-type%23test_national_id|ERR-ENROLL-001", + }, + "status": "enrolled", + } + + response = self.url_open( + self.api_base_url, + data=json.dumps(payload), + headers=self._get_headers(), + ) + + self.assertEqual(response.status_code, 422) + + def test_update_program_membership_bad_identifier_returns_400(self): + """PUT with an identifier lacking the 'system|value' separator returns 400""" + url = f"{self.api_base_url}/no-pipe-identifier" + + payload = { + "type": "ProgramMembership", + "program": {"reference": "Program/urn:openspp:program|test-enrollment-program"}, + "beneficiary": { + "reference": "Individual/urn:openspp:vocab:id-type%23test_national_id|ENROLL-001", + }, + "status": "paused", + } + + response = self.url_put(url, data=json.dumps(payload), headers=self._get_headers()) + self.assertEqual(response.status_code, 400) + + def test_update_program_membership_unknown_program_returns_422(self): + """PUT whose payload references a non-existent program hits the update error path (422)""" + url = f"{self.api_base_url}/urn:openspp:vocab:id-type%23test_national_id|ENROLL-001" + + payload = { + "type": "ProgramMembership", + "program": {"reference": "Program/urn:openspp:program|does-not-exist-program"}, + "beneficiary": { + "reference": "Individual/urn:openspp:vocab:id-type%23test_national_id|ENROLL-001", + }, + "status": "paused", + } + + response = self.url_put(url, data=json.dumps(payload), headers=self._get_headers()) + self.assertEqual(response.status_code, 422) + + def test_search_program_memberships_prev_link(self): + """Search with a non-zero _offset builds a previous-page link""" + # Seed a few more memberships so paging is meaningful + for i in range(3): + ind = self.create_test_individual(identifier_value=f"PREV-{i}") + self.create_test_membership(partner=ind, program=self.program) + self.create_consent( + registrant=ind, + grantee_partner=self.client.partner_id, + resource_type="all", + field_access="all", + ) + + url = f"{self.api_base_url}?_count=2&_offset=2" + response = self.url_open(url, headers=self._get_headers()) + + self.assertEqual(response.status_code, 200) + data = json.loads(response.content) + self.assertIn("links", data) + self.assertIsNotNone(data["links"].get("prev")) def test_consent_filtering_applied(self): """Without consent, read returns 403 (same as individual endpoint pattern)""" diff --git a/spp_studio_programs/tests/__init__.py b/spp_studio_programs/tests/__init__.py index fbc72b00f..4459c4581 100644 --- a/spp_studio_programs/tests/__init__.py +++ b/spp_studio_programs/tests/__init__.py @@ -1,3 +1,4 @@ # Part of OpenSPP. See LICENSE file for full copyright and licensing details. from . import test_studio_mixin_programs +from . import test_pack_install_wizard_programs diff --git a/spp_studio_programs/tests/test_pack_install_wizard_programs.py b/spp_studio_programs/tests/test_pack_install_wizard_programs.py new file mode 100644 index 000000000..a45612e00 --- /dev/null +++ b/spp_studio_programs/tests/test_pack_install_wizard_programs.py @@ -0,0 +1,33 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""The pack-install wizard's program_id and _get_pack_program_id hook.""" + +from odoo.tests import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestPackInstallWizardPrograms(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.Wizard = cls.env["spp.studio.pack.install.wizard"] + cls.program = cls.env["spp.program"].create( + { + "name": "Pack Lookup Program", + "target_type": "individual", + } + ) + + def test_program_id_field_added(self): + """The companion adds the optional program_id field to the wizard.""" + self.assertIn("program_id", self.Wizard._fields) + self.assertEqual(self.Wizard._fields["program_id"].comodel_name, "spp.program") + + def test_get_pack_program_id_returns_selected_program(self): + """_get_pack_program_id returns the selected program id.""" + wizard = self.Wizard.new({"program_id": self.program.id}) + self.assertEqual(wizard._get_pack_program_id(), self.program.id) + + def test_get_pack_program_id_none_when_unset(self): + """_get_pack_program_id returns None when no program is selected.""" + wizard = self.Wizard.new({}) + self.assertIsNone(wizard._get_pack_program_id())