Skip to content

Commit 98a45a9

Browse files
Merge pull request #173 from OpenSPP/fix/enroll-eligible-async-none-state
fix(spp_programs): handle state=None in enroll eligible async dispatch
2 parents 74f3225 + adcaa4a commit 98a45a9

5 files changed

Lines changed: 95 additions & 12 deletions

File tree

spp_programs/README.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,16 @@ Dependencies
254254
Changelog
255255
=========
256256

257+
19.0.2.0.11
258+
~~~~~~~~~~~
259+
260+
- Fix ``TypeError: 'NoneType' object is not iterable`` when clicking
261+
**Enroll Eligible** on programs with at least 200 beneficiaries (async
262+
dispatch path)
263+
- Mirror ``get_beneficiaries`` semantics in
264+
``_enroll_eligible_registrants_async``: when ``state`` is ``None``,
265+
omit the state filter instead of crashing on ``tuple(None)``
266+
257267
19.0.2.0.10
258268
~~~~~~~~~~~
259269

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/readme/HISTORY.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
### 19.0.2.0.11
2+
3+
- Fix `TypeError: 'NoneType' object is not iterable` when clicking **Enroll Eligible** on programs with at least 200 beneficiaries (async dispatch path)
4+
- Mirror `get_beneficiaries` semantics in `_enroll_eligible_registrants_async`: when `state` is `None`, omit the state filter instead of crashing on `tuple(None)`
5+
16
### 19.0.2.0.10
27

38
- Increase parallel-safe channel limits (cycle, eligibility_manager, program_manager) from 1 to 4

spp_programs/static/description/index.html

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,17 @@ <h2><a class="toc-backref" href="#toc-entry-1">Changelog</a></h2>
658658
</div>
659659
</div>
660660
<div class="section" id="section-1">
661+
<h1>19.0.2.0.11</h1>
662+
<ul class="simple">
663+
<li>Fix <tt class="docutils literal">TypeError: 'NoneType' object is not iterable</tt> when clicking
664+
<strong>Enroll Eligible</strong> on programs with at least 200 beneficiaries (async
665+
dispatch path)</li>
666+
<li>Mirror <tt class="docutils literal">get_beneficiaries</tt> semantics in
667+
<tt class="docutils literal">_enroll_eligible_registrants_async</tt>: when <tt class="docutils literal">state</tt> is <tt class="docutils literal">None</tt>,
668+
omit the state filter instead of crashing on <tt class="docutils literal">tuple(None)</tt></li>
669+
</ul>
670+
</div>
671+
<div class="section" id="section-2">
661672
<h1>19.0.2.0.10</h1>
662673
<ul class="simple">
663674
<li>Increase parallel-safe channel limits (cycle, eligibility_manager,
@@ -670,7 +681,7 @@ <h1>19.0.2.0.10</h1>
670681
submission on double-click</li>
671682
</ul>
672683
</div>
673-
<div class="section" id="section-2">
684+
<div class="section" id="section-3">
674685
<h1>19.0.2.0.9</h1>
675686
<ul class="simple">
676687
<li>Add context flags (<tt class="docutils literal">skip_registrant_statistics</tt>,
@@ -683,7 +694,7 @@ <h1>19.0.2.0.9</h1>
683694
<tt class="docutils literal">_compute_has_members</tt></li>
684695
</ul>
685696
</div>
686-
<div class="section" id="section-3">
697+
<div class="section" id="section-4">
687698
<h1>19.0.2.0.8</h1>
688699
<ul class="simple">
689700
<li>Replace OFFSET pagination with NTILE-based ID-range batching in all
@@ -694,7 +705,7 @@ <h1>19.0.2.0.8</h1>
694705
program and cycle</li>
695706
</ul>
696707
</div>
697-
<div class="section" id="section-4">
708+
<div class="section" id="section-5">
698709
<h1>19.0.2.0.7</h1>
699710
<ul class="simple">
700711
<li>Bulk membership creation using raw SQL INSERT ON CONFLICT DO NOTHING
@@ -703,7 +714,7 @@ <h1>19.0.2.0.7</h1>
703714
<tt class="docutils literal">_add_beneficiaries</tt> with bulk SQL path</li>
704715
</ul>
705716
</div>
706-
<div class="section" id="section-5">
717+
<div class="section" id="section-6">
707718
<h1>19.0.2.0.6</h1>
708719
<ul class="simple">
709720
<li>Remove unused entitlement_base_model.py (dead code, never imported)</li>
@@ -712,34 +723,34 @@ <h1>19.0.2.0.6</h1>
712723
payment, and fund tests (172 → 492 tests)</li>
713724
</ul>
714725
</div>
715-
<div class="section" id="section-6">
726+
<div class="section" id="section-7">
716727
<h1>19.0.2.0.5</h1>
717728
<ul class="simple">
718729
<li>Batch create entitlements and payments instead of one-by-one ORM
719730
creates</li>
720731
</ul>
721732
</div>
722-
<div class="section" id="section-7">
733+
<div class="section" id="section-8">
723734
<h1>19.0.2.0.4</h1>
724735
<ul class="simple">
725736
<li>Fetch fund balance once per approval batch instead of per entitlement</li>
726737
</ul>
727738
</div>
728-
<div class="section" id="section-8">
739+
<div class="section" id="section-9">
729740
<h1>19.0.2.0.3</h1>
730741
<ul class="simple">
731742
<li>Replace cycle computed fields (total_amount, entitlements_count,
732743
approval flags) with SQL aggregation queries</li>
733744
</ul>
734745
</div>
735-
<div class="section" id="section-9">
746+
<div class="section" id="section-10">
736747
<h1>19.0.2.0.2</h1>
737748
<ul class="simple">
738749
<li>Add composite indexes for frequent query patterns on entitlements and
739750
program memberships</li>
740751
</ul>
741752
</div>
742-
<div class="section" id="section-10">
753+
<div class="section" id="section-11">
743754
<h1>19.0.2.0.1</h1>
744755
<ul class="simple">
745756
<li>Replace Python-level uniqueness checks with SQL UNIQUE constraints for
@@ -748,7 +759,7 @@ <h1>19.0.2.0.1</h1>
748759
constraint creation</li>
749760
</ul>
750761
</div>
751-
<div class="section" id="section-11">
762+
<div class="section" id="section-12">
752763
<h1>19.0.2.0.0</h1>
753764
<ul class="simple">
754765
<li>Initial migration to OpenSPP2</li>

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)