Skip to content

Commit 8fa3c4f

Browse files
fix(spp_programs): handle state=None in enroll eligible async dispatch
The "Enroll Eligible" button calls enroll_eligible_registrants() with no argument, so state defaults to None. When the program has at least MIN_ROW_JOB_QUEUE (200) beneficiaries, the async path runs and called tuple(states) on None, raising TypeError. Mirror get_beneficiaries semantics: when states is None/empty, omit the "state IN %s" filter from the SQL where-clause so all states are considered. Add a regression test that covers state=None.
1 parent 2e377b6 commit 8fa3c4f

2 files changed

Lines changed: 59 additions & 2 deletions

File tree

spp_programs/models/managers/program_manager.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,11 +188,20 @@ def _enroll_eligible_registrants_async(self, states, members_count):
188188
if isinstance(states, str):
189189
states = [states]
190190

191+
# Mirror get_beneficiaries: when states is None/empty, no state filter is
192+
# applied (i.e. all states). Otherwise restrict to the given states.
193+
if states:
194+
where_clause = "program_id = %s AND state IN %s"
195+
params = (program.id, tuple(states))
196+
else:
197+
where_clause = "program_id = %s"
198+
params = (program.id,)
199+
191200
id_ranges = compute_id_ranges(
192201
self.env.cr,
193202
"spp_program_membership",
194-
"program_id = %s AND state IN %s",
195-
(program.id, tuple(states)),
203+
where_clause,
204+
params,
196205
self.MAX_ROW_JOB_QUEUE,
197206
)
198207

spp_programs/tests/test_keyset_pagination.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,3 +497,51 @@ def test_enroll_eligible_async_handles_string_state(self):
497497
# Verify the states param was converted from string to tuple
498498
call_params = mock_ranges.call_args[0][3]
499499
self.assertIsInstance(call_params[1], tuple)
500+
501+
def test_enroll_eligible_async_handles_none_state(self):
502+
"""_enroll_eligible_registrants_async must handle state=None.
503+
504+
The UI "Enroll Eligible" button calls enroll_eligible_registrants() with no
505+
argument. When the program has >= MIN_ROW_JOB_QUEUE beneficiaries, the async
506+
path runs with state=None — it must not crash on `tuple(None)`.
507+
"""
508+
partners = self.env["res.partner"].create(
509+
[{"name": f"Registrant {i}", "is_registrant": True} for i in range(5)]
510+
)
511+
self.env["spp.program.membership"].create(
512+
[
513+
{
514+
"partner_id": p.id,
515+
"program_id": self.program.id,
516+
"state": "draft",
517+
}
518+
for p in partners
519+
]
520+
)
521+
522+
manager = self.env["spp.program.manager.default"].create(
523+
{
524+
"name": "Test Manager",
525+
"program_id": self.program.id,
526+
}
527+
)
528+
529+
with patch(
530+
"odoo.addons.spp_programs.models.managers.program_manager.compute_id_ranges",
531+
return_value=[(1, 5)],
532+
) as mock_ranges:
533+
with patch.object(type(manager), "delayable", return_value=manager):
534+
try:
535+
manager._enroll_eligible_registrants_async(None, 5)
536+
except TypeError as e:
537+
self.fail(f"async dispatch must accept state=None, got TypeError: {e}")
538+
except Exception: # pylint: disable=except-pass
539+
pass
540+
541+
mock_ranges.assert_called_once()
542+
# When states is None, the where clause must omit "state IN %s" and
543+
# params must contain only the program id (no states tuple).
544+
where_clause = mock_ranges.call_args[0][2]
545+
call_params = mock_ranges.call_args[0][3]
546+
self.assertNotIn("state IN", where_clause)
547+
self.assertEqual(call_params, (self.program.id,))

0 commit comments

Comments
 (0)