Apply strategies
Contents
Apply strategies#
The apply strategy determines what happens when an approved change request is applied. This reference covers the two strategy types (field mapping and custom), the base class contract, and patterns from the built-in strategies.
If you have not built a CR type before, start with the Tutorial: build a transfer member CR type.
Field mapping vs. custom#
Choose the strategy type based on what the apply action needs to do:
Use field mapping when... |
Use a custom strategy when... |
|---|---|
Copying field values from the detail to the registrant |
Creating or deleting records |
Renaming fields (e.g., |
Updating multiple models in one operation |
Applying expression-based transforms |
Changing relationships (memberships, roles) |
Clearing registrant fields |
Changing record status (active/inactive) |
Merging or splitting records |
|
Any logic beyond field-to-field copying |
Field mapping is configured entirely in XML — no Python required. See Field mappings for the configuration guide.
Custom strategies require a Python class. The rest of this page covers how to build them.
Base class contract#
All custom strategies inherit from spp.cr.strategy.base, which defines three methods:
class SPPCRStrategyBase(models.AbstractModel):
_name = "spp.cr.strategy.base"
def apply(self, change_request):
"""Apply the change request. Required.
Args:
change_request: The spp.change.request record
Returns:
True on success
Raises:
UserError: If apply fails
"""
raise NotImplementedError("Subclasses must implement apply()")
def preview(self, change_request):
"""Preview what changes will be applied. Optional.
Returns:
dict describing planned changes
"""
return {}
def validate(self, change_request):
"""Custom validation before apply. Optional.
Raises:
ValidationError: If validation fails
"""
pass
Only apply() is required. The framework calls apply() when the CR is applied and preview() when a reviewer requests a preview. validate() is a hook provided for your convenience — the framework does not call it automatically. If you want pre-apply validation, either call self.validate(change_request) at the top of your own apply() method, or put the validation checks directly in apply().
Anatomy of a custom strategy#
Every custom strategy follows the same structure. Here is the add_member strategy, which creates a new individual and adds them to a group:
import logging
from odoo import Command, _, fields, models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class SPPCRApplyAddMember(models.AbstractModel):
_name = "spp.cr.apply.add_member"
_inherit = "spp.cr.strategy.base"
_description = "CR Apply: Add Group Member"
def apply(self, change_request):
# 1. Get the registrant and detail record
group = change_request.registrant_id
detail = change_request.get_detail()
# 2. Validate preconditions
if not group.is_group:
raise UserError(_("Registrant must be a group."))
if not detail:
raise UserError(_("No detail record found."))
if not detail.member_name:
raise UserError(_("Member name is required."))
# 3. Execute changes
individual_vals = {
"name": detail.member_name,
"given_name": detail.given_name,
"family_name": detail.family_name,
"birthdate": detail.birthdate,
"is_registrant": True,
"is_group": False,
}
if detail.gender_id:
individual_vals["gender_id"] = detail.gender_id.id
individual = self.env["res.partner"].create(individual_vals)
membership_vals = {
"group": group.id,
"individual": individual.id,
"start_date": fields.Datetime.now(),
}
if detail.relationship_id:
membership_vals["membership_type_ids"] = [
Command.link(detail.relationship_id.id)
]
self.env["spp.group.membership"].create(membership_vals)
# 4. Store results on the detail record
detail.write({"created_individual_id": individual.id})
# 5. Log the operation
_logger.info(
"Added member partner_id=%s to group partner_id=%s via CR %s",
individual.id, group.id, change_request.name,
)
return True
The five-step pattern#
Get registrant and detail —
change_request.registrant_idandchange_request.get_detail()Validate preconditions — check everything before making changes; raise
UserErroron failureExecute changes — create, update, or delete records as needed
Store results — write back to the detail record (e.g.,
created_individual_id) so the UI can display what was createdLog — use
_logger.info()for audit trail in the server log
Warning
Strategies run with sudo(). The CR framework calls strategy.apply() with full system privileges. This is by design: the approval workflow is the security gate, not the strategy. Do not add permission checks inside your strategy — they would be redundant and could prevent legitimate applies.
Preview pattern#
The preview() method returns a dict that the CR UI displays to reviewers. Include an _action key to identify the type of change, and human-readable values for the rest:
def preview(self, change_request):
detail = change_request.get_detail()
if not detail:
return {}
return {
"_action": "create_member",
"member_name": detail.member_name,
"group": change_request.registrant_id.name,
"relationship": (
detail.relationship_id.display
if detail.relationship_id else None
),
}
Two valid preview shapes#
The preview() method can return either shape, and the CR UI picks the appropriate renderer:
Action shape (shown above, with
_actionkey) — the UI renders an action summary. Best for strategies that don't map cleanly to field-by-field comparisons (create_member, split_household, etc.).Field-diff shape —
{field_name: {"old": current_value, "new": new_value}, ...}— the UI renders a side-by-side comparison table. Best for field-mapping-like strategies.
The field mapping strategy uses the field-diff shape automatically, computing old/new values from the registered mappings.
Registering a strategy#
In your module's XML data, the CR type record connects the strategy to the detail model:
<record id="cr_type_add_member" model="spp.change.request.type">
<field name="name">Add Group Member</field>
<field name="code">add_member</field>
<field name="detail_model">spp.cr.detail.add_member</field>
<field name="apply_strategy">custom</field>
<field name="apply_model">spp.cr.apply.add_member</field>
<!-- ... other fields ... -->
</record>
The two key fields:
apply_strategy— set tocustomto use a Python strategy class (orfield_mappingfor configuration-only)apply_model— the_nameof yourAbstractModelstrategy class
Built-in strategies by pattern#
The built-in strategies fall into four categories:
Field copy (no custom code)#
Strategy |
CR type |
What it does |
|---|---|---|
|
edit_individual, edit_group |
Copies mapped fields from detail to registrant, with optional expression transforms |
Record creation#
Strategy |
CR type |
What it does |
|---|---|---|
|
add_member |
Creates individual + group membership |
|
create_group |
Creates a new group registrant |
|
update_id |
Creates or updates DMS document records |
Relationship changes#
Strategy |
CR type |
What it does |
|---|---|---|
|
transfer_member |
Ends source membership, creates target membership |
|
remove_member |
Ends a group membership |
|
change_hoh |
Swaps the "head" membership type between two individuals |
Status and complex operations#
Strategy |
CR type |
What it does |
|---|---|---|
|
exit_registrant |
Deactivates a registrant record |
|
merge_registrants |
Consolidates two registrant records into one |
|
split_household |
Splits a group into two groups, moving selected members |
Manual (no-op)#
The manual apply strategy does nothing — the administrator must apply changes manually outside the system. Use this for CR types that track requests without automated application.
openspp.org