Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions spp_change_request_v2/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,33 @@ Before declaring a new CR type complete:
Changelog
=========

19.0.3.1.17
~~~~~~~~~~~

- fix(change_request): a submitted request that lost its detail row can
be opened again on a real deployment. 19.0.3.1.10 let
``_ensure_detail()`` bind a new detail past submission, but it then
prefilled the row from the registrant with a write, and the
detail-level freeze refused that write whenever the request type
carries field mappings — which every shipped Edit Individual / Edit
Group type does through ``spp_cr_types_base``. The module's own test
type had no mappings, so the repair passed in isolation and failed
everywhere else. The prefill now goes into the detail's ``create()``
(``_prefill_values()`` feeds it), so the repair performs no
post-submit write at all and the freeze keeps no exemption: a repaired
row arrives already carrying what the registrant holds — leaving it
empty was never an option, approving that would clear every mapped
field — and every later write to a mapped field on a submitted
request, the registrant's own value included, stays refused. Where the
prefill cannot cover a mapped field — a legacy registrant holding a
future date of birth, which the prefill declines to offer — the
rebuilt row would propose clearing it, so a post-submit repair of a
field-mapping request is refused with an explicit message instead, and
the request has to be reset to draft; a post-submit repair is also
logged. The frozen-detail tests give the test types the shipped
mappings so this shape is exercised in module CI, and apply a repaired
request end to end to prove the registrant is untouched (#443)

19.0.3.1.16
~~~~~~~~~~~

Expand Down
2 changes: 1 addition & 1 deletion spp_change_request_v2/__manifest__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "OpenSPP Change Request V2",
"version": "19.0.3.1.16",
"version": "19.0.3.1.17",
"sequence": 50,
"category": "OpenSPP",
"summary": "Configuration-driven change request system with UX improvements, conflict detection and duplicate prevention",
Expand Down
52 changes: 47 additions & 5 deletions spp_change_request_v2/models/change_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -855,15 +855,57 @@ def _ensure_detail(self):

# Use sudo() for creation - users don't need create permission
# Detail records are always created by the system automatically
detail = detail_model.sudo().create({cr_field: self.id}) # nosemgrep: odoo-sudo-without-context
vals = {cr_field: self.id}
# Pre-fill from the registrant in the same create() rather than by a
# later write(): this also runs to repair a request that was already
# submitted, and the detail-level freeze refuses every post-submit
# write to a mapped field. The prefill copies what the registrant
# already holds, so it proposes nothing, while a repaired row left
# empty would, on approval, clear every mapped field.
prefill = {}
if hasattr(detail_model, "_prefill_values"):
# nosemgrep: odoo-sudo-without-context
prefill = detail_model.sudo().new(vals)._prefill_values()
# nosemgrep: odoo-sudo-without-context
detail = detail_model.sudo().create({**vals, **prefill})
self.detail_res_id = detail.id

# Pre-fill detail from registrant if the detail model supports it
if hasattr(detail, "prefill_from_registrant"):
detail.prefill_from_registrant()
if self.approval_state and self.approval_state not in ("draft", "revision"):
if self.request_type_id.apply_strategy == "field_mapping":
self._assert_reconstructed_detail_proposes_nothing()
_logger.warning(
"Change request %s: detail record reconstructed after submission",
self.name,
)

return self.get_detail()

def _assert_reconstructed_detail_proposes_nothing(self):
"""A field-mapping detail rebuilt for a submitted request must not carry a proposal.

The rebuilt row holds the registrant's current values, so approving
it applies nothing. That holds only as far as the prefill covers the
apply mapping: a value the prefill declines to offer (a future date of
birth, or a mapping added later for a field the prefill does not know)
would be applied as "clear this field", because the field-mapping
strategy writes empty values on purpose. Nobody can correct the row
afterwards, since it is frozen, so the request is refused instead and
has to be reset to draft.
"""
self.ensure_one()
sudo_rec = self.sudo() # nosemgrep: odoo-sudo-without-context
changes = dict(sudo_rec.request_type_id.get_apply_strategy().preview(sudo_rec) or {})
changes.pop("_action", None)
changes.pop("_message", None)
if changes:
raise UserError(
_(
"The details of this submitted change request were lost and cannot be "
"reconstructed without proposing a change to: %(fields)s. "
"Reset the request to draft and enter the details again.",
fields=", ".join(sorted(changes)),
)
)

# ══════════════════════════════════════════════════════════════════════════
# APPROVAL ACTIONS
# ══════════════════════════════════════════════════════════════════════════
Expand Down
43 changes: 28 additions & 15 deletions spp_change_request_v2/models/change_request_detail_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,10 @@ def _assert_content_editable(self, vals):

Mirrors the view-level readonly (approval_state not in draft/revision) at
the server so it cannot be bypassed via RPC. Editing requires resetting
the CR to draft, which re-routes the approval.
the CR to draft, which re-routes the approval. There is no exemption:
the registrant prefill of a repaired detail happens inside ``create()``
(``_ensure_detail`` passes ``_prefill_values()`` to it), so it never
reaches this guard.
"""
for rec in self:
change_request = rec.change_request_id
Expand Down Expand Up @@ -274,27 +277,37 @@ def _get_prefill_mapping(self):
"""
return {}

def prefill_from_registrant(self):
"""Pre-fill detail fields from registrant.

This method updates the current record with values from the registrant
based on the mapping defined in _get_prefill_mapping().
def _prefill_values(self):
"""The registrant's current values for the fields in _get_prefill_mapping().

Override _get_prefill_mapping() in detail models to enable prefilling.
Works on a ``new()`` record as well as a stored one, so ``_ensure_detail``
can pass the result straight into ``create()``; only truthy registrant
values are included.
"""
self.ensure_one()
if not self.registrant_id:
return

mapping = self._get_prefill_mapping()
if not mapping:
return
return {}

values = {}
for detail_field, registrant_field in mapping.items():
for detail_field, registrant_field in self._get_prefill_mapping().items():
registrant_value = getattr(self.registrant_id, registrant_field, False)
if registrant_value:
values[detail_field] = registrant_value
if not registrant_value:
continue
if isinstance(registrant_value, models.BaseModel):
# create() takes an id where write() also accepts a recordset.
registrant_value = registrant_value.id
values[detail_field] = registrant_value
return values

def prefill_from_registrant(self):
"""Pre-fill detail fields from registrant.

This method updates the current record with values from the registrant
based on the mapping defined in _get_prefill_mapping(). It is a write,
so on a submitted request the detail-level freeze applies to it.

Override _get_prefill_mapping() in detail models to enable prefilling.
"""
values = self._prefill_values()
if values:
self.write(values)
4 changes: 4 additions & 0 deletions spp_change_request_v2/readme/HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### 19.0.3.1.17

- fix(change_request): a submitted request that lost its detail row can be opened again on a real deployment. 19.0.3.1.10 let `_ensure_detail()` bind a new detail past submission, but it then prefilled the row from the registrant with a write, and the detail-level freeze refused that write whenever the request type carries field mappings — which every shipped Edit Individual / Edit Group type does through `spp_cr_types_base`. The module's own test type had no mappings, so the repair passed in isolation and failed everywhere else. The prefill now goes into the detail's `create()` (`_prefill_values()` feeds it), so the repair performs no post-submit write at all and the freeze keeps no exemption: a repaired row arrives already carrying what the registrant holds — leaving it empty was never an option, approving that would clear every mapped field — and every later write to a mapped field on a submitted request, the registrant's own value included, stays refused. Where the prefill cannot cover a mapped field — a legacy registrant holding a future date of birth, which the prefill declines to offer — the rebuilt row would propose clearing it, so a post-submit repair of a field-mapping request is refused with an explicit message instead, and the request has to be reset to draft; a post-submit repair is also logged. The frozen-detail tests give the test types the shipped mappings so this shape is exercised in module CI, and apply a repaired request end to end to prove the registrant is untouched (#443)

### 19.0.3.1.16

- fix(change_request): an Edit Individual change request can be opened again on a registrant that already holds a future date of birth. `spp.change.request.create` prefills the detail from the registrant and that prefill is a write, so the guard added in 19.0.3.1.15 refused the copied value and the request could not be created at all — closing the very path field staff use to correct the date. A birthdate the guard would refuse is now dropped from the prefill mapping rather than offered, so the field arrives empty and a valid date has to be entered. The rule itself lives in one place (`_is_future_birthdate` on the mixin), so what prefill declines to offer and what the constraint refuses cannot drift apart.
Expand Down
Loading
Loading