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
24 changes: 24 additions & 0 deletions spp_programs/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,30 @@ Dependencies
Changelog
=========

19.0.2.3.0
~~~~~~~~~~

- feat(spp_programs): **Duplicate Detection is a card with an Add
dialog.** Adding a method asks which method and what to call it,
instead of editing a Reference field that exposed the model/record
plumbing — and could quietly wire another program's method into this
one. Selecting **ID document** now also asks which ID types to
compare, without which the method matched nothing and reported no
duplicates at all (#1171)
- fix(spp_programs): **Deduplicate now clears flags it no longer
finds.** A membership marked as duplicated stayed that way after the
clash behind it was fixed, because a membership already in that state
was never re-evaluated. Each run recomputes rather than accumulates
(#796)
- feat(spp_programs): **validators can return duplicated memberships to
draft in bulk** — a row button on the membership list and a Back to
Draft server action bound to it, so a whole selection can be cleared
instead of opening records one at a time (#1170)
- fix(spp_programs): a removed deduplication method can be added again.
Removing a row unlinked it without deleting it, and the duplicate
check counted the leftover, so the method the card no longer showed
still blocked its own re-adding (#1171)

19.0.2.2.1
~~~~~~~~~~

Expand Down
3 changes: 2 additions & 1 deletion spp_programs/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"name": "OpenSPP Programs",
"summary": "Manage programs, cycles, beneficiary enrollment, entitlements (cash and in-kind), payments, and fund tracking for social protection.",
"category": "OpenSPP/Core",
"version": "19.0.2.2.1",
"version": "19.0.2.3.0",
"sequence": 1,
"author": "OpenSPP.org",
"website": "https://github.com/OpenSPP/OpenSPP2",
Expand Down Expand Up @@ -109,6 +109,7 @@
"wizard/reject_entitlement_wizard.xml",
"wizard/reject_inkind_entitlement_wizard.xml",
"wizard/reset_to_pending_wizard.xml",
"wizard/deduplication_setup_wizard.xml",
"wizard/create_program_wizard_compliance_views.xml",
"wizard/create_program_wizard_cel_views.xml",
"wizard/enrollment_wizard_views.xml",
Expand Down
59 changes: 55 additions & 4 deletions spp_programs/models/program_manager_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ class ProgramManagerUI(models.Model):
payment_manager_display = fields.Char(compute="_compute_banner_layout_helpers")
payment_manager_detail = fields.Text(compute="_compute_banner_layout_helpers")

deduplication_manager_count = fields.Integer(compute="_compute_banner_layout_helpers")
deduplication_manager_display = fields.Char(compute="_compute_banner_layout_helpers")
deduplication_manager_detail = fields.Text(compute="_compute_banner_layout_helpers")

@api.depends("eligibility_manager_ids", "eligibility_manager_ids.manager_ref_id")
def _compute_eligibility_summary(self):
for rec in self:
Expand Down Expand Up @@ -413,6 +417,7 @@ def _compute_banner_layout_helpers(self):
("cycle_manager_ids", "cycle"),
("compliance_manager_ids", "compliance"),
("payment_manager_ids", "payment"),
("deduplication_manager_ids", "deduplication"),
)
for rec in self:
for field_name, prefix in banners:
Expand Down Expand Up @@ -595,13 +600,35 @@ def action_configure_payment(self):
return False

def action_configure_deduplication(self):
"""Open deduplication manager configuration."""
"""Open deduplication configuration.

Unlike compliance or payment, a program may have several deduplication
methods — by ID and by phone, say. Opening ``[0]`` would silently edit
the first and leave the rest unreachable, so the card only offers this
button when there is exactly one; with several, each method has its own
cog in the card body (OP#1171).
"""
self.ensure_one()
readonly = not self.can_edit_configuration
if self.deduplication_manager_ids and self.deduplication_manager_ids[0].manager_ref_id:
return self.deduplication_manager_ids[0].open_manager_form(readonly=readonly, title=_("Deduplication"))
configured = self.deduplication_manager_ids.filtered(lambda wrapper: wrapper.manager_ref_id)
if len(configured) == 1:
return configured.open_manager_form(readonly=readonly, title=_("Deduplication"))
if len(configured) > 1:
# Not target="new": a list in a dialog cannot drill into a form, so
# it renders as a dead end — rows look clickable and do nothing.
# Opening it in the breadcrumb keeps the rows navigable. The card
# itself lists the methods with their own buttons, so this is a
# fallback for programmatic callers rather than the normal route.
return {
"type": "ir.actions.act_window",
"name": _("Duplicate Detection"),
"res_model": "spp.deduplication.manager",
"view_mode": "list,form",
"domain": [("id", "in", configured.ids)],
"context": {"create": False, "default_program_id": self.id},
}
if not readonly:
return self._open_manager_setup_wizard("deduplication")
return self.action_add_deduplication_manager()
return False

def action_configure_notification(self):
Expand Down Expand Up @@ -692,6 +719,30 @@ def action_add_payment_manager(self):
},
}

def action_add_deduplication_manager(self):
"""Open the two-step dialog for adding a deduplication method (OP#1171).

Compliance and Payment open their single concrete model directly
(#952, #953). Deduplication has three methods rather than one, so the
dialog has to ask which before it can ask for a name — the wizard does
both, then creates the concrete record with the context that makes
`source_mixin.create()` build the wrapper alongside it.

Adding is offered even when a method already exists, because a program
may legitimately check by ID *and* by phone.
"""
self.ensure_one()
if not self.can_edit_configuration:
return False
return {
"type": "ir.actions.act_window",
"name": _("Add a Deduplication Method"),
"res_model": "spp.deduplication.setup.wizard",
"view_mode": "form",
"target": "new",
"context": {"default_program_id": self.id},
}

def _open_manager_setup_wizard(self, manager_type):
"""Open wizard to set up a new manager of the specified type."""
return {
Expand Down
38 changes: 31 additions & 7 deletions spp_programs/models/program_membership.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@

_logger = logging.getLogger(__name__)

# States a membership may be returned to draft from. Both are dead ends
# otherwise: deduplication and eligibility checks put a membership here and
# nothing ever took it out again (OP#1170).
RESETTABLE_TO_DRAFT = ("duplicated", "not_eligible")


class SPPProgramMembership(models.Model):
_inherit = [
Expand Down Expand Up @@ -354,6 +359,12 @@ def deduplicate_beneficiaries(self):
message = None
kind = "success"
if len(deduplication_managers):
# The managers work across the whole program, not just this
# membership, so clear the program's flags first and let the run
# re-apply them. Without this a membership stays "duplicated" long
# after the clash behind it was fixed (OP#796).
self.program_id._reset_duplicate_flags()

states = ["draft", "enrolled", "eligible", "paused", "duplicated"]
duplicates = 0
for el in deduplication_managers:
Expand Down Expand Up @@ -384,13 +395,26 @@ def deduplicate_beneficiaries(self):
}

def back_to_draft(self):
"""Reset membership to draft state."""
self.write(
{
"state": "draft",
}
)
return
"""Return duplicated or not-eligible memberships to draft.

Deliberately recordset-safe: the "Back to Draft" server action hands
this a whole selection, and it is offered from lists as well as from
the membership form.

Guarded because this is now reachable from four places rather than one.
It used to write ``draft`` over any state at all, so a stray call could
quietly undo an enrolment or reopen an exit (OP#1170).
"""
blocked = self.filtered(lambda membership: membership.state not in RESETTABLE_TO_DRAFT)
if blocked:
raise UserError(
_(
"Only duplicated or not-eligible memberships can be returned to draft. "
"%s of the selected memberships are in another state.",
len(blocked),
)
)
self.write({"state": "draft"})

def action_pause(self):
"""Pause the membership."""
Expand Down
55 changes: 42 additions & 13 deletions spp_programs/models/programs.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,36 +448,65 @@ def verify_eligibility(self):
else:
raise UserError(_("No Program Manager defined."))

def _duplicated_memberships(self):
"""The memberships this program currently has flagged as duplicates."""
self.ensure_one()
return self.env["spp.program.membership"].search([("program_id", "=", self.id), ("state", "=", "duplicated")])

def _reset_duplicate_flags(self):
"""Clear every duplicate flag so a run can re-apply it (OP#796).

Deduplication only ever added the flag. A membership marked duplicated
stayed that way even once the clash behind it was fixed — correct the
ID or the phone number, run Deduplicate again, and nothing happened,
because a membership already in ``duplicated`` is never re-evaluated
out of it.

Clearing first turns the run into a recompute: whoever still clashes is
flagged again by the managers a moment later, and whoever no longer does
is simply left in draft. The whole thing is one transaction, so a run
that fails part-way leaves the flags as they were.

Returns the memberships that were flagged going in, so the caller can
report what the run actually resolved.
"""
self.ensure_one()
flagged = self._duplicated_memberships()
if flagged:
flagged.back_to_draft()
return flagged

def deduplicate_beneficiaries(self):
for rec in self:
deduplication_managers = rec.get_managers(self.MANAGER_DEDUPLICATION)
message = None
kind = "success"
if len(deduplication_managers):
# Count already-flagged duplicates before running
already_duplicated = self.env["spp.program.membership"].search_count(
[("program_id", "=", rec.id), ("state", "=", "duplicated")]
)
# Flagged going in — cleared now, and re-applied below to
# whoever is still a duplicate.
previously_flagged = rec._reset_duplicate_flags()

states = ["draft", "enrolled", "eligible", "paused", "duplicated"]
duplicates = 0
for el in deduplication_managers:
duplicates += el.deduplicate_beneficiaries(states)

# Count total duplicates after running
total_duplicated = self.env["spp.program.membership"].search_count(
[("program_id", "=", rec.id), ("state", "=", "duplicated")]
)
new_duplicates = total_duplicated - already_duplicated
currently_flagged = rec._duplicated_memberships()
total_duplicated = len(currently_flagged)
still_flagged = len(previously_flagged & currently_flagged)
new_duplicates = len(currently_flagged - previously_flagged)
resolved = len(previously_flagged - currently_flagged)

if total_duplicated > 0:
if total_duplicated > 0 or resolved:
parts = []
if new_duplicates > 0:
parts.append(_("%(new)s new duplicate(s) found", new=new_duplicates))
if already_duplicated > 0:
parts.append(_("%(existing)s already flagged", existing=already_duplicated))
if still_flagged > 0:
parts.append(_("%(existing)s still flagged", existing=still_flagged))
if resolved > 0:
parts.append(_("%(resolved)s no longer duplicate(s)", resolved=resolved))
message = ", ".join(parts) + "."
kind = "warning"
kind = "warning" if total_duplicated else "success"
elif duplicates > 0:
message = _(
"Found %(count)s duplicate beneficiaries.",
Expand Down
7 changes: 7 additions & 0 deletions spp_programs/readme/HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
### 19.0.2.3.0

- feat(spp_programs): **Duplicate Detection is a card with an Add dialog.** Adding a method asks which method and what to call it, instead of editing a Reference field that exposed the model/record plumbing — and could quietly wire another program's method into this one. Selecting **ID document** now also asks which ID types to compare, without which the method matched nothing and reported no duplicates at all (#1171)
- fix(spp_programs): **Deduplicate now clears flags it no longer finds.** A membership marked as duplicated stayed that way after the clash behind it was fixed, because a membership already in that state was never re-evaluated. Each run recomputes rather than accumulates (#796)
- feat(spp_programs): **validators can return duplicated memberships to draft in bulk** — a row button on the membership list and a Back to Draft server action bound to it, so a whole selection can be cleared instead of opening records one at a time (#1170)
- fix(spp_programs): a removed deduplication method can be added again. Removing a row unlinked it without deleting it, and the duplicate check counted the leftover, so the method the card no longer showed still blocked its own re-adding (#1171)

### 19.0.2.2.1

- fix(spp_programs): stop Enroll Eligible undoing a deliberate pause. A paused membership is now left alone wherever eligibility is re-run — the enrol pass, the disenrol sweep that would otherwise have moved it to Not Eligible, and the per-membership methods reachable over RPC. Pausing is a decision that only Resume reverses (#1117)
Expand Down
3 changes: 3 additions & 0 deletions spp_programs/security/ir.model.access.csv
Original file line number Diff line number Diff line change
Expand Up @@ -404,3 +404,6 @@ access_spp_prepare_entitlement_confirm_wizard_validator,Prepare Entitlement Conf
access_spp_program_membership_exit_wizard_officer,Program Membership Exit Wizard Officer Access,spp_programs.model_spp_program_membership_exit_wizard,spp_programs.group_programs_officer,1,1,1,0
access_spp_program_membership_exit_wizard_manager,Program Membership Exit Wizard Manager Access,spp_programs.model_spp_program_membership_exit_wizard,spp_programs.group_programs_manager,1,1,1,1
access_spp_program_membership_exit_wizard_admin,Program Membership Exit Wizard Admin Access,spp_programs.model_spp_program_membership_exit_wizard,spp_security.group_spp_admin,1,1,1,1
access_spp_deduplication_setup_wizard_manager,Deduplication Setup Wizard Manager Access,spp_programs.model_spp_deduplication_setup_wizard,group_programs_manager,1,1,1,1
access_spp_deduplication_setup_wizard_validator,Deduplication Setup Wizard Validator Access,spp_programs.model_spp_deduplication_setup_wizard,group_programs_validator,1,1,1,0
access_spp_deduplication_setup_wizard_admin,Deduplication Setup Wizard Admin Access,spp_programs.model_spp_deduplication_setup_wizard,spp_security.group_spp_admin,1,1,1,1
Loading
Loading