Skip to content

feat(spp_change_request_v2): add dynamic approval-aware conflict & duplicate detection #84

Merged
gonzalesedwin1123 merged 7 commits into19.0from
feat/dynamic-approval-conflict-detection
Mar 10, 2026
Merged

feat(spp_change_request_v2): add dynamic approval-aware conflict & duplicate detection #84
gonzalesedwin1123 merged 7 commits into19.0from
feat/dynamic-approval-conflict-detection

Conversation

@gonzalesedwin1123
Copy link
Member

Summary

  • Add dynamic approval framework: CR types can be configured so users select a single field to modify per CR, with approval workflow determined by CEL condition evaluation on candidate definitions
  • Update conflict and duplicate detection to narrow scope to only the selected field for dynamic-approval CRs (prefilled fields are ignored)
  • Add field change tracking on CRs (selected field name, old value, new value) with automatic sync from detail records
  • Add UI for configuring dynamic approval on CR types and displaying field changes on CR forms

Changes

Models

  • CR Type: use_dynamic_approval toggle, candidate_definition_ids (Many2many to approval definitions with CEL conditions)
  • CR: selected_field_name, selected_field_old_value, selected_field_new_value fields; _resolve_dynamic_approval() with CEL evaluation; _compute_field_values_for_cel() with value
    normalization (Many2one, vocabulary codes, hierarchical parents)
  • Detail Base: field_to_modify selection field; write()/create() overrides to sync selection to parent CR
  • Conflict Mixin: _filter_by_field_conflicts() narrows to selected field for dynamic CRs; _calculate_similarity() compares only selected field
  • Conflict Model: Skip conflict checks at create time for dynamic CRs; trigger re-check when selected_field_name is written

Views

  • CR type form: dynamic approval section in Approval tab with candidate definitions and CEL variable documentation
  • CR form: "Field Change" group showing selected field, old value, and new value

Tests (28 new tests)

  • 16 tests for dynamic approval routing, CEL evaluation, field sync, value normalization, multi-tier approval, E2E workflow
  • 12 tests for dynamic-aware conflict detection (field-level, cross-type, timing) and duplicate similarity scoring

Documentation

  • USAGE.md: Developer guide with 3 progressive examples (basic, multi-tier, dynamic approval), CEL variable reference, and implementation checklist

Test plan

  • All 267 tests pass (0 failures, 0 errors)
  • Linting passes (ruff, ruff-format, prettier)
  • No existing tests removed or weakened
  • Code review: no critical issues

Add dynamic approval framework to CR types and change requests:
- CR type: use_dynamic_approval toggle and candidate_definition_ids
- CR: selected_field_name/old_value/new_value for field tracking
- Detail base: field_to_modify selection, sync to parent CR on write/create
- Dynamic approval resolution via CEL condition evaluation on candidates
- Value normalization for CEL (Many2one, vocabulary codes, hierarchical)
…icate detection

Update conflict and duplicate detection to narrow scope for dynamic CRs:
- Conflict field filtering uses only selected_field_name for dynamic types
- Duplicate similarity compares only the selected field (0% for different fields)
- Skip conflict checks at create time for dynamic CRs (field not set yet)
- Trigger conflict re-check when selected_field_name is written
Add UI for dynamic approval configuration and field change display:
- CR type form: dynamic approval toggle, candidate definitions, CEL help text
- CR form: field change group showing selected field, old value, new value
…ion tests

28 tests covering:
- Dynamic approval routing, CEL evaluation, field sync, value normalization
- Multi-tier approval with dynamic routing, E2E workflow
- Dynamic-aware conflict detection (field-level, cross-type, timing)
- Dynamic-aware duplicate detection (similarity scoring)
Add USAGE.md with three progressive examples (basic, multi-tier, dynamic
approval), CEL variable reference, and implementation checklist. Update
DESCRIPTION.md to mention dynamic approval routing capability.
Auto-generated by oca-gen-addon-readme from updated DESCRIPTION.md
and new USAGE.md fragments.
@codecov
Copy link

codecov bot commented Mar 10, 2026

Codecov Report

❌ Patch coverage is 93.63057% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.15%. Comparing base (32329d6) to head (9da3e01).
⚠️ Report is 8 commits behind head on 19.0.

Files with missing lines Patch % Lines
spp_change_request_v2/models/change_request.py 93.42% 5 Missing ⚠️
...ge_request_v2/models/change_request_detail_base.py 91.66% 4 Missing ⚠️
spp_change_request_v2/models/conflict_mixin.py 96.29% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0      #84      +/-   ##
==========================================
+ Coverage   68.43%   69.15%   +0.72%     
==========================================
  Files         562      628      +66     
  Lines       31853    35632    +3779     
==========================================
+ Hits        21798    24643    +2845     
- Misses      10055    10989     +934     
Flag Coverage Δ
spp_api_v2_change_request 60.29% <ø> (ø)
spp_base_common 90.26% <ø> (ø)
spp_change_request_v2 73.93% <93.63%> (?)
spp_cr_types_advanced 0.00% <ø> (?)
spp_cr_types_base 0.00% <ø> (?)
spp_dci_demo 69.23% <ø> (ø)
spp_mis_demo_v2 69.82% <ø> (ø)
spp_programs 45.51% <ø> (ø)
spp_security 66.66% <ø> (ø)
spp_starter_social_registry 0.00% <ø> (?)
spp_starter_sp_mis 81.25% <ø> (?)
spp_studio_change_requests 84.67% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...hange_request_v2/models/change_request_conflict.py 74.75% <100.00%> (ø)
...pp_change_request_v2/models/change_request_type.py 66.88% <100.00%> (ø)
spp_change_request_v2/models/conflict_mixin.py 90.53% <96.29%> (ø)
...ge_request_v2/models/change_request_detail_base.py 69.49% <91.66%> (ø)
spp_change_request_v2/models/change_request.py 83.21% <93.42%> (ø)

... and 61 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gonzalesedwin1123 gonzalesedwin1123 marked this pull request as ready for review March 10, 2026 08:22
@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the change request system by introducing a dynamic approval framework. This allows for highly flexible and context-sensitive approval workflows, where the specific field being modified dictates the routing logic through Common Expression Language (CEL) conditions. Concurrently, the system's conflict and duplicate detection capabilities have been made 'approval-aware,' ensuring that these checks accurately reflect the dynamically selected field, thereby improving the precision and relevance of these safeguards.

Highlights

  • Dynamic Approval Framework: Introduced a dynamic approval framework where Change Request (CR) types can be configured to allow users to select a single field for modification, with the approval workflow determined by Common Expression Language (CEL) condition evaluation on candidate definitions.
  • Conflict and Duplicate Detection Refinement: Updated conflict and duplicate detection mechanisms to narrow their scope for dynamic-approval CRs, focusing checks only on the selected field and ignoring prefilled fields to prevent false positives.
  • Field Change Tracking: Added new fields to CRs to track the selected field name, its old value, and its new value, with automatic synchronization from detail records for audit trails and CEL evaluation.
  • User Interface Enhancements: Implemented UI for configuring dynamic approval settings on CR types and displaying the tracked field changes directly on CR forms.
  • Comprehensive Documentation and Testing: Expanded documentation with a new developer guide covering static, multi-tier, and dynamic approval, including CEL variable references. Added 28 new tests to validate dynamic approval routing, CEL evaluation, field sync, value normalization, multi-tier workflows, and dynamic-aware conflict/duplicate detection.
Changelog
  • spp_change_request_v2/README.rst
    • Updated key capabilities to include dynamic approval.
    • Added a comprehensive developer guide for creating custom CR types.
  • spp_change_request_v2/models/change_request.py
    • Added fields for selected field name, old value, and new value for dynamic approval tracking.
    • Modified CR creation to skip conflict detection for dynamic approval CRs until the selected field is set.
    • Implemented dynamic approval routing logic using CEL evaluation of candidate definitions.
    • Added methods to compute and normalize field values for CEL expressions.
  • spp_change_request_v2/models/change_request_conflict.py
    • Modified CR creation to skip conflict checks for dynamic approval CRs.
    • Updated CR write method to re-run conflict checks when the selected field changes.
  • spp_change_request_v2/models/change_request_detail_base.py
    • Added related boolean field 'use_dynamic_approval' and selection field 'field_to_modify'.
    • Introduced a method to define selectable fields for dynamic approval.
    • Overrode write and create methods to synchronize selected field and its values to the parent CR.
    • Added a helper method to format field values for human-readable display.
  • spp_change_request_v2/models/change_request_type.py
    • Added 'use_dynamic_approval' boolean field to enable dynamic approval.
    • Added 'candidate_definition_ids' Many2many field to link approval definitions for dynamic routing.
  • spp_change_request_v2/models/conflict_mixin.py
    • Adjusted conflict filtering to consider only the selected field for dynamic CRs.
    • Modified similarity calculation for duplicates to compare only the selected field in dynamic CRs.
  • spp_change_request_v2/readme/DESCRIPTION.md
    • Updated the module description to highlight the new dynamic approval feature.
  • spp_change_request_v2/readme/USAGE.md
    • Added a new developer guide for creating custom CR types, including examples for static, multi-tier, and dynamic approval.
  • spp_change_request_v2/static/description/index.html
    • Updated the HTML description to include dynamic approval in the key capabilities.
  • spp_change_request_v2/tests/init.py
    • Added new test modules for dynamic approval and dynamic-aware conflict detection.
  • spp_change_request_v2/tests/test_conflict_dynamic_approval.py
    • Added new tests for dynamic-approval-aware conflict and duplicate detection scenarios.
  • spp_change_request_v2/tests/test_dynamic_approval.py
    • Added new tests for dynamic approval routing, field synchronization, CEL evaluation, and end-to-end workflows.
  • spp_change_request_v2/views/change_request_type_views.xml
    • Added UI elements for configuring dynamic approval on CR types, including a toggle and candidate definition selection.
    • Included an informational alert with details on CEL variables for dynamic approval.
  • spp_change_request_v2/views/change_request_views.xml
    • Added a 'Field Change' group to the CR form to display the selected field and its old/new values.
Activity
  • A new dynamic approval framework has been integrated, allowing for flexible approval routing based on specific field changes.
  • Conflict and duplicate detection logic has been updated to intelligently handle dynamic approval scenarios, preventing irrelevant checks.
  • New fields were added to change requests to track the specific field being modified and its old/new values, enhancing auditability.
  • User interface components were introduced to configure dynamic approval on CR types and visualize field changes on CR forms.
  • Extensive documentation, including a developer guide, was added to support the new features.
  • A comprehensive suite of 28 new tests was developed to ensure the robustness and correctness of the dynamic approval and related detection mechanisms.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

…thods

Add 12 tests covering previously uncovered branches in change_request.py:
- _normalize_value_for_cel: char, date, boolean, integer, many2many,
  falsy boolean/integer, None without record, hierarchical parent
- _compute_field_values_for_cel: no field selected
- _resolve_dynamic_approval: no selected field
- _check_can_submit: dynamic CR without field selection
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a significant and well-designed dynamic approval framework, allowing change request workflows to be determined by the specific field being modified. The implementation is robust, covering CEL-based condition evaluation, field change tracking, and updates to conflict and duplicate detection to be approval-aware. The addition of comprehensive tests (28 new tests) and detailed developer documentation in USAGE.md is excellent.

I have two suggestions for improvement:

  1. A high-severity issue in the duplicate detection logic for "mixed" cases (dynamic vs. static CRs), which could lead to inaccurate similarity scores.
  2. A medium-severity suggestion to refactor the write method in change_request_detail_base.py for better readability and maintainability.

Overall, this is a high-quality contribution that greatly enhances the flexibility of the change request system.

Comment on lines +461 to 479
# Dynamic approval: compare only the selected field
my_selected = self.selected_field_name
other_selected = other_cr.selected_field_name
if my_selected and other_selected:
# Different fields selected = not duplicates
if my_selected != other_selected:
return 0.0
# Same field: compare that field's value only
if my_selected in my_detail._fields and my_selected in other_detail._fields:
my_value = self._normalize_field_value(getattr(my_detail, my_selected, None))
other_value = self._normalize_field_value(getattr(other_detail, my_selected, None))
if my_value == other_value:
return 100.0
elif self._are_similar(my_value, other_value):
return 80.0
return 0.0

# Static CRs (or mixed): original logic
check_fields = config.get_check_fields_list()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current logic for duplicate detection in _calculate_similarity correctly handles the case where both CRs are dynamic. However, it doesn't correctly handle the "mixed" case where one CR is dynamic and the other is static. In this scenario, it falls back to the original logic, which compares all configured check_fields. This can lead to inflated similarity scores because pre-filled fields on the dynamic CR (which are not being changed) will match pre-filled fields on the static CR.

For example, if a dynamic CR changes given_name and a static CR changes family_name, the pre-filled phone number on both will match, incorrectly increasing their similarity.

The logic should be updated to only compare the fields that are effectively being changed in each CR. For a dynamic CR, that's just the selected_field_name. For a static CR, it's the fields defined in the duplicate configuration. The comparison should be on the intersection of these effective fields.

Here is a suggested implementation for the entire _calculate_similarity method that addresses this:

def _calculate_similarity(self, other_cr, config):
    self.ensure_one()

    my_detail = self.get_detail()
    other_detail = other_cr.get_detail()

    if not my_detail or not other_detail:
        return 0.0

    my_selected = self.selected_field_name
    other_selected = other_cr.selected_field_name
    config_fields = set(config.get_check_fields_list())

    # Determine effective fields to compare for each CR.
    my_fields = {my_selected} if my_selected else config_fields
    other_fields = {other_selected} if other_selected else config_fields

    # Fallback for static CRs with no fields configured: compare all storable fields.
    if not my_selected and not my_fields:
        my_fields = {
            f for f in my_detail._fields if my_detail._fields[f].store and f not in ('id', 'create_date', 'write_date', 'create_uid', 'write_uid', 'change_request_id', 'registrant_id', 'approval_state', 'is_applied')
        }
    if not other_selected and not other_fields:
        other_fields = {
            f for f in other_detail._fields if other_detail._fields[f].store and f not in ('id', 'create_date', 'write_date', 'create_uid', 'write_uid', 'change_request_id', 'registrant_id', 'approval_state', 'is_applied')
        }

    # The fields to compare are the intersection of what each CR is proposing to change.
    fields_to_check = my_fields & other_fields

    if not fields_to_check:
        return 0.0

    total_fields = len(fields_to_check)
    matching_score = 0.0

    for field_name in fields_to_check:
        if field_name not in my_detail._fields or field_name not in other_detail._fields:
            continue

        my_value = self._normalize_field_value(getattr(my_detail, field_name, None))
        other_value = self._normalize_field_value(getattr(other_detail, field_name, None))

        if my_value == other_value:
            matching_score += 1.0
        elif self._are_similar(my_value, other_value):
            matching_score += 0.8

    return (matching_score / total_fields) * 100.0 if total_fields > 0 else 0.0

Comment on lines +75 to +85
def write(self, vals):
result = super().write(vals)
if "field_to_modify" in vals:
for rec in self:
if rec.change_request_id:
rec._sync_field_to_modify()
else:
for rec in self:
if rec.field_to_modify and rec.field_to_modify in vals and rec.change_request_id:
rec._sync_field_to_modify()
return result

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The write method can be simplified to improve readability and avoid iterating over self in two separate blocks. The current implementation correctly handles syncing when field_to_modify or its target field's value changes, but the logic can be combined into a single loop for better maintainability.

Suggested change
def write(self, vals):
result = super().write(vals)
if "field_to_modify" in vals:
for rec in self:
if rec.change_request_id:
rec._sync_field_to_modify()
else:
for rec in self:
if rec.field_to_modify and rec.field_to_modify in vals and rec.change_request_id:
rec._sync_field_to_modify()
return result
def write(self, vals):
result = super().write(vals)
for rec in self:
if not rec.change_request_id:
continue
# Sync if the field selector is changed, or if the value of the selected field is changed.
if "field_to_modify" in vals or (rec.field_to_modify and rec.field_to_modify in vals):
rec._sync_field_to_modify()
return result

@emjay0921 emjay0921 self-requested a review March 10, 2026 08:53
@gonzalesedwin1123 gonzalesedwin1123 merged commit a4f3701 into 19.0 Mar 10, 2026
27 checks passed
@gonzalesedwin1123 gonzalesedwin1123 deleted the feat/dynamic-approval-conflict-detection branch March 10, 2026 09:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants