From 228ac0c0969720d1b6b377879196086f541b47d1 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Fri, 6 Feb 2026 14:58:01 +0800 Subject: [PATCH] fix(spp_api_v2_change_request): fix 3 bugs and promote to Beta - Fix path routing: replace /{reference} with /{p1}/{p2}/{p3} on all 8 reference-based endpoints since CR references (CR/2026/00001) contain slashes that Starlette splits into separate path segments - Fix mail.thread AccessError: add mail_create_nolog and tracking_disable context to service env, preventing flush_all() from triggering message_partner_ids writes as Public user (uid=3) - Fix detail serialization: exclude mail.thread fields (message_ids, message_is_follower, has_message, etc.) from API responses - Promote development_status from Alpha to Beta --- spp_api_v2_change_request/README.rst | 79 +++++++++---------- spp_api_v2_change_request/__manifest__.py | 4 +- .../routers/change_request.py | 61 ++++++++++---- .../services/change_request_service.py | 21 ++++- .../static/description/index.html | 13 +-- 5 files changed, 106 insertions(+), 72 deletions(-) diff --git a/spp_api_v2_change_request/README.rst b/spp_api_v2_change_request/README.rst index da93d18..c1ae8cc 100644 --- a/spp_api_v2_change_request/README.rst +++ b/spp_api_v2_change_request/README.rst @@ -14,9 +14,9 @@ OpenSPP API V2 - Change Request !! source digest: sha256:ac0243c931848a9215f89c328fce09d312e30b92ac9c3c1f141bfe26b4fef453 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status - :alt: Alpha + :alt: Beta .. |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 @@ -35,17 +35,16 @@ numbers (CR/2024/00001) instead of database IDs for all operations. Key Capabilities ~~~~~~~~~~~~~~~~ -- Create change requests in draft status with registrant and detail - data -- Read individual change requests by reference or search with filters - (registrant, type, status, dates) -- Update detail data on draft change requests with optimistic locking - via If-Match headers -- Submit draft requests for approval workflow -- Approve, reject, or request revision on pending requests (requires - approval scope) -- Apply approved change requests to registrant records -- Reset rejected/revision requests to draft for resubmission +- Create change requests in draft status with registrant and detail data +- Read individual change requests by reference or search with filters + (registrant, type, status, dates) +- Update detail data on draft change requests with optimistic locking + via If-Match headers +- Submit draft requests for approval workflow +- Approve, reject, or request revision on pending requests (requires + approval scope) +- Apply approved change requests to registrant records +- Reset rejected/revision requests to draft for resubmission Key Models ~~~~~~~~~~ @@ -78,26 +77,26 @@ To configure OAuth 2.0 clients with appropriate scopes: by ``spp_api_v2``) 2. Configure OAuth 2.0 clients with appropriate scopes: - - ``change_request:read`` - Read and search change requests - - ``change_request:create`` - Create new change requests - - ``change_request:update`` - Update, submit, and reset requests - - ``change_request:approve`` - Approve, reject, or request revision - - ``change_request:apply`` - Apply approved changes to registrants + - ``change_request:read`` - Read and search change requests + - ``change_request:create`` - Create new change requests + - ``change_request:update`` - Update, submit, and reset requests + - ``change_request:approve`` - Approve, reject, or request revision + - ``change_request:apply`` - Apply approved changes to registrants API Endpoints ~~~~~~~~~~~~~ -- ``POST /ChangeRequest`` - Create new change request -- ``GET /ChangeRequest/{reference}`` - Read by reference -- ``GET /ChangeRequest`` - Search with filters -- ``PUT /ChangeRequest/{reference}`` - Update detail data -- ``POST /ChangeRequest/{reference}/$submit`` - Submit for approval -- ``POST /ChangeRequest/{reference}/$approve`` - Approve request -- ``POST /ChangeRequest/{reference}/$reject`` - Reject request -- ``POST /ChangeRequest/{reference}/$request-revision`` - Request - revision -- ``POST /ChangeRequest/{reference}/$apply`` - Apply to registrant -- ``POST /ChangeRequest/{reference}/$reset`` - Reset to draft +- ``POST /ChangeRequest`` - Create new change request +- ``GET /ChangeRequest/{reference}`` - Read by reference +- ``GET /ChangeRequest`` - Search with filters +- ``PUT /ChangeRequest/{reference}`` - Update detail data +- ``POST /ChangeRequest/{reference}/$submit`` - Submit for approval +- ``POST /ChangeRequest/{reference}/$approve`` - Approve request +- ``POST /ChangeRequest/{reference}/$reject`` - Reject request +- ``POST /ChangeRequest/{reference}/$request-revision`` - Request + revision +- ``POST /ChangeRequest/{reference}/$apply`` - Apply to registrant +- ``POST /ChangeRequest/{reference}/$reset`` - Reset to draft Security ~~~~~~~~ @@ -110,12 +109,12 @@ enforces scope checks on each endpoint. Users must authenticate via the Extension Points ~~~~~~~~~~~~~~~~ -- Inherit ``ChangeRequestService`` to customize serialization, - validation, or business logic -- Override router endpoint functions to add custom validation or side - effects -- Extend the API schema by inheriting the Pydantic models in - ``schemas/change_request.py`` +- Inherit ``ChangeRequestService`` to customize serialization, + validation, or business logic +- Override router endpoint functions to add custom validation or side + effects +- Extend the API schema by inheriting the Pydantic models in + ``schemas/change_request.py`` UI Location ~~~~~~~~~~~ @@ -129,11 +128,6 @@ Dependencies ``spp_api_v2``, ``spp_change_request_v2`` -.. IMPORTANT:: - This is an alpha version, the data model and design can change at any time without warning. - Only for development or testing purpose, do not use in production. - `More details on development status `_ - **Table of contents** .. contents:: @@ -169,10 +163,13 @@ Maintainers .. |maintainer-reichie020212| image:: https://github.com/reichie020212.png?size=40px :target: https://github.com/reichie020212 :alt: reichie020212 +.. |maintainer-emjay0921| image:: https://github.com/emjay0921.png?size=40px + :target: https://github.com/emjay0921 + :alt: emjay0921 Current maintainers: -|maintainer-jeremi| |maintainer-gonzalesedwin1123| |maintainer-reichie020212| +|maintainer-jeremi| |maintainer-gonzalesedwin1123| |maintainer-reichie020212| |maintainer-emjay0921| This module is part of the `OpenSPP/OpenSPP2 `_ project on GitHub. diff --git a/spp_api_v2_change_request/__manifest__.py b/spp_api_v2_change_request/__manifest__.py index da90fc1..a9865dd 100644 --- a/spp_api_v2_change_request/__manifest__.py +++ b/spp_api_v2_change_request/__manifest__.py @@ -6,8 +6,8 @@ "author": "OpenSPP.org", "website": "https://github.com/OpenSPP/OpenSPP2", "license": "LGPL-3", - "development_status": "Alpha", - "maintainers": ["jeremi", "gonzalesedwin1123", "reichie020212"], + "development_status": "Beta", + "maintainers": ["jeremi", "gonzalesedwin1123", "reichie020212", "emjay0921"], "depends": [ "spp_api_v2", "spp_change_request_v2", diff --git a/spp_api_v2_change_request/routers/change_request.py b/spp_api_v2_change_request/routers/change_request.py index 4cd065c..5f17b23 100644 --- a/spp_api_v2_change_request/routers/change_request.py +++ b/spp_api_v2_change_request/routers/change_request.py @@ -40,6 +40,11 @@ change_request_router = APIRouter(tags=["ChangeRequest"], prefix="/ChangeRequest") +def _build_reference(p1: str, p2: str, p3: str) -> str: + """Reconstruct CR reference from path segments (e.g., CR/2026/00001).""" + return f"{p1}/{p2}/{p3}" + + @change_request_router.post( "", response_model=ChangeRequestResponse, @@ -87,9 +92,11 @@ async def create_change_request( return service.to_api_schema(cr) -@change_request_router.get("/{reference}", response_model=ChangeRequestResponse) +@change_request_router.get("/{p1}/{p2}/{p3}", response_model=ChangeRequestResponse) async def read_change_request( - reference: Annotated[str, Path(description="CR reference (e.g., CR/2024/00001)")], + p1: Annotated[str, Path(description="Reference part 1 (e.g., CR)")], + p2: Annotated[str, Path(description="Reference part 2 (e.g., 2026)")], + p3: Annotated[str, Path(description="Reference part 3 (e.g., 00001)")], env: Annotated[Environment, Depends(odoo_env)], api_client: Annotated[dict, Depends(get_authenticated_client)], response: Response, @@ -106,6 +113,7 @@ async def read_change_request( detail="Client does not have permission to read change requests", ) + reference = _build_reference(p1, p2, p3) service = ChangeRequestService(env) cr = service.find_by_reference(reference) @@ -224,9 +232,11 @@ async def search_change_requests( ) -@change_request_router.put("/{reference}", response_model=ChangeRequestResponse) +@change_request_router.put("/{p1}/{p2}/{p3}", response_model=ChangeRequestResponse) async def update_change_request( - reference: Annotated[str, Path()], + p1: Annotated[str, Path(description="Reference part 1 (e.g., CR)")], + p2: Annotated[str, Path(description="Reference part 2 (e.g., 2026)")], + p3: Annotated[str, Path(description="Reference part 3 (e.g., 00001)")], update_data: ChangeRequestUpdate, env: Annotated[Environment, Depends(odoo_env)], api_client: Annotated[dict, Depends(get_authenticated_client)], @@ -245,6 +255,7 @@ async def update_change_request( detail="Client does not have permission to update change requests", ) + reference = _build_reference(p1, p2, p3) service = ChangeRequestService(env) cr = service.find_by_reference(reference) @@ -284,9 +295,11 @@ async def update_change_request( # === Actions === -@change_request_router.post("/{reference}/$submit", response_model=ChangeRequestResponse) +@change_request_router.post("/{p1}/{p2}/{p3}/$submit", response_model=ChangeRequestResponse) async def submit_change_request( - reference: Annotated[str, Path()], + p1: Annotated[str, Path(description="Reference part 1 (e.g., CR)")], + p2: Annotated[str, Path(description="Reference part 2 (e.g., 2026)")], + p3: Annotated[str, Path(description="Reference part 3 (e.g., 00001)")], env: Annotated[Environment, Depends(odoo_env)], api_client: Annotated[dict, Depends(get_authenticated_client)], ): @@ -301,6 +314,7 @@ async def submit_change_request( detail="Client does not have permission to submit change requests", ) + reference = _build_reference(p1, p2, p3) service = ChangeRequestService(env) cr = service.find_by_reference(reference) @@ -321,9 +335,11 @@ async def submit_change_request( return service.to_api_schema(cr) -@change_request_router.post("/{reference}/$approve", response_model=ChangeRequestResponse) +@change_request_router.post("/{p1}/{p2}/{p3}/$approve", response_model=ChangeRequestResponse) async def approve_change_request( - reference: Annotated[str, Path()], + p1: Annotated[str, Path(description="Reference part 1 (e.g., CR)")], + p2: Annotated[str, Path(description="Reference part 2 (e.g., 2026)")], + p3: Annotated[str, Path(description="Reference part 3 (e.g., 00001)")], env: Annotated[Environment, Depends(odoo_env)], api_client: Annotated[dict, Depends(get_authenticated_client)], action_data: ApproveActionData | None = None, @@ -339,6 +355,7 @@ async def approve_change_request( detail="Client does not have permission to approve change requests", ) + reference = _build_reference(p1, p2, p3) service = ChangeRequestService(env) cr = service.find_by_reference(reference) @@ -360,9 +377,11 @@ async def approve_change_request( return service.to_api_schema(cr) -@change_request_router.post("/{reference}/$reject", response_model=ChangeRequestResponse) +@change_request_router.post("/{p1}/{p2}/{p3}/$reject", response_model=ChangeRequestResponse) async def reject_change_request( - reference: Annotated[str, Path()], + p1: Annotated[str, Path(description="Reference part 1 (e.g., CR)")], + p2: Annotated[str, Path(description="Reference part 2 (e.g., 2026)")], + p3: Annotated[str, Path(description="Reference part 3 (e.g., 00001)")], action_data: RejectActionData, env: Annotated[Environment, Depends(odoo_env)], api_client: Annotated[dict, Depends(get_authenticated_client)], @@ -378,6 +397,7 @@ async def reject_change_request( detail="Client does not have permission to reject change requests", ) + reference = _build_reference(p1, p2, p3) service = ChangeRequestService(env) cr = service.find_by_reference(reference) @@ -398,9 +418,11 @@ async def reject_change_request( return service.to_api_schema(cr) -@change_request_router.post("/{reference}/$request-revision", response_model=ChangeRequestResponse) +@change_request_router.post("/{p1}/{p2}/{p3}/$request-revision", response_model=ChangeRequestResponse) async def request_revision_change_request( - reference: Annotated[str, Path()], + p1: Annotated[str, Path(description="Reference part 1 (e.g., CR)")], + p2: Annotated[str, Path(description="Reference part 2 (e.g., 2026)")], + p3: Annotated[str, Path(description="Reference part 3 (e.g., 00001)")], action_data: RequestRevisionActionData, env: Annotated[Environment, Depends(odoo_env)], api_client: Annotated[dict, Depends(get_authenticated_client)], @@ -416,6 +438,7 @@ async def request_revision_change_request( detail="Client does not have permission to request revision", ) + reference = _build_reference(p1, p2, p3) service = ChangeRequestService(env) cr = service.find_by_reference(reference) @@ -436,9 +459,11 @@ async def request_revision_change_request( return service.to_api_schema(cr) -@change_request_router.post("/{reference}/$apply", response_model=ChangeRequestResponse) +@change_request_router.post("/{p1}/{p2}/{p3}/$apply", response_model=ChangeRequestResponse) async def apply_change_request( - reference: Annotated[str, Path()], + p1: Annotated[str, Path(description="Reference part 1 (e.g., CR)")], + p2: Annotated[str, Path(description="Reference part 2 (e.g., 2026)")], + p3: Annotated[str, Path(description="Reference part 3 (e.g., 00001)")], env: Annotated[Environment, Depends(odoo_env)], api_client: Annotated[dict, Depends(get_authenticated_client)], ): @@ -453,6 +478,7 @@ async def apply_change_request( detail="Client does not have permission to apply change requests", ) + reference = _build_reference(p1, p2, p3) service = ChangeRequestService(env) cr = service.find_by_reference(reference) @@ -473,9 +499,11 @@ async def apply_change_request( return service.to_api_schema(cr) -@change_request_router.post("/{reference}/$reset", response_model=ChangeRequestResponse) +@change_request_router.post("/{p1}/{p2}/{p3}/$reset", response_model=ChangeRequestResponse) async def reset_change_request( - reference: Annotated[str, Path()], + p1: Annotated[str, Path(description="Reference part 1 (e.g., CR)")], + p2: Annotated[str, Path(description="Reference part 2 (e.g., 2026)")], + p3: Annotated[str, Path(description="Reference part 3 (e.g., 00001)")], env: Annotated[Environment, Depends(odoo_env)], api_client: Annotated[dict, Depends(get_authenticated_client)], ): @@ -490,6 +518,7 @@ async def reset_change_request( detail="Client does not have permission to reset change requests", ) + reference = _build_reference(p1, p2, p3) service = ChangeRequestService(env) cr = service.find_by_reference(reference) diff --git a/spp_api_v2_change_request/services/change_request_service.py b/spp_api_v2_change_request/services/change_request_service.py index 3472dd5..dafebe2 100644 --- a/spp_api_v2_change_request/services/change_request_service.py +++ b/spp_api_v2_change_request/services/change_request_service.py @@ -18,7 +18,8 @@ class ChangeRequestService: """Service for Change Request resource CRUD and mapping.""" def __init__(self, env: Environment): - self.env = env + ctx = dict(env.context, mail_create_nolog=True, tracking_disable=True) + self.env = env(context=ctx) def find_by_reference(self, reference: str): """ @@ -161,7 +162,7 @@ def _serialize_detail(self, detail) -> dict[str, Any]: # Get fields from the model model_fields = detail._fields - # Skip internal and computed fields + # Skip internal, computed, and mail.thread fields skip_fields = { "id", "create_uid", @@ -175,6 +176,20 @@ def _serialize_detail(self, detail) -> dict[str, Any]: "registrant_id", "approval_state", "is_applied", + # mail.thread fields + "message_ids", + "message_follower_ids", + "message_partner_ids", + "message_is_follower", + "has_message", + "message_needaction", + "message_needaction_counter", + "message_has_error", + "message_has_error_counter", + "message_attachment_count", + "message_has_sms_error", + "message_main_attachment_id", + "website_message_ids", } for field_name, field in model_fields.items(): @@ -451,5 +466,5 @@ def apply(self, cr): def reset_to_draft(self, cr): """Reset rejected/revision CR to draft.""" if cr.approval_state not in ("rejected", "revision"): - raise UserError("Only rejected or revision-requested change requests " "can be reset to draft") + raise UserError("Only rejected or revision-requested change requests can be reset to draft") cr.action_reset_to_draft() diff --git a/spp_api_v2_change_request/static/description/index.html b/spp_api_v2_change_request/static/description/index.html index 0462e9c..e6abd0b 100644 --- a/spp_api_v2_change_request/static/description/index.html +++ b/spp_api_v2_change_request/static/description/index.html @@ -374,7 +374,7 @@

OpenSPP API V2 - Change Request

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! source digest: sha256:ac0243c931848a9215f89c328fce09d312e30b92ac9c3c1f141bfe26b4fef453 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Alpha License: LGPL-3 OpenSPP/OpenSPP2

+

Beta License: LGPL-3 OpenSPP/OpenSPP2

Exposes REST API endpoints for managing change requests through the OpenSPP API V2 framework. Allows external systems to create change requests, submit them for approval, and apply approved changes to @@ -383,8 +383,7 @@

OpenSPP API V2 - Change Request

Key Capabilities

    -
  • Create change requests in draft status with registrant and detail -data
  • +
  • Create change requests in draft status with registrant and detail data
  • Read individual change requests by reference or search with filters (registrant, type, status, dates)
  • Update detail data on draft change requests with optimistic locking @@ -488,12 +487,6 @@

    UI Location

    Dependencies

    spp_api_v2, spp_change_request_v2

    -
    -

    Important

    -

    This is an alpha version, the data model and design can change at any time without warning. -Only for development or testing purpose, do not use in production. -More details on development status

    -

    Table of contents