Skip to content

Commit 08d14e4

Browse files
kneckinatorgonzalesedwin1123
authored andcommitted
perf(spp_programs): batch create entitlements and payments
Cash entitlement manager now collects all entitlement dicts and calls create() once instead of per-beneficiary. Payment manager collects all payment dicts per batch tag and calls create() once, then batch-assigns via write(). Also prefetches bank_ids to avoid N+1 queries.
1 parent 5916d10 commit 08d14e4

4 files changed

Lines changed: 217 additions & 28 deletions

File tree

spp_programs/models/managers/entitlement_manager_cash.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,13 +156,16 @@ def prepare_entitlements(self, cycle, beneficiaries):
156156
if addl_fields:
157157
new_entitlements_to_create[beneficiary_id.id].update(addl_fields)
158158

159-
# Create entitlement records
159+
# Create entitlement records in a single batch
160+
vals_list = []
160161
for ent in new_entitlements_to_create:
161162
initial_amount = new_entitlements_to_create[ent]["initial_amount"]
162163
new_entitlements_to_create[ent]["initial_amount"] = self._check_subsidy(initial_amount)
163164
# Create non-zero entitlements only
164165
if new_entitlements_to_create[ent]["initial_amount"] > 0.0:
165-
self.env["spp.entitlement"].create(new_entitlements_to_create[ent])
166+
vals_list.append(new_entitlements_to_create[ent])
167+
if vals_list:
168+
self.env["spp.entitlement"].create(vals_list)
166169

167170
def _get_addl_entitlement_fields(self, beneficiary_id):
168171
"""

spp_programs/models/managers/payment_manager.py

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -211,42 +211,55 @@ def _prepare_payments(self, cycle, entitlements):
211211
entitlements -= tag_entitlements
212212
max_batch_size = batch_tag.max_batch_size
213213

214-
for i, entitlement_id in enumerate(tag_entitlements):
215-
payment = self.env["spp.payment"].create(
214+
if not tag_entitlements:
215+
continue
216+
217+
# Prefetch bank_ids to avoid N+1 queries
218+
tag_entitlements.mapped("partner_id.bank_ids.acc_number")
219+
220+
# Build all payment vals in one pass
221+
payment_vals_list = []
222+
for entitlement_id in tag_entitlements:
223+
account_number = None
224+
if entitlement_id.partner_id.bank_ids:
225+
account_number = entitlement_id.partner_id.bank_ids[0].acc_number
226+
payment_vals_list.append(
216227
{
217228
"name": str(uuid4()),
218229
"entitlement_id": entitlement_id.id,
219230
"cycle_id": entitlement_id.cycle_id.id,
220231
"amount_issued": entitlement_id.initial_amount,
221232
"payment_fee": entitlement_id.transfer_fee,
222233
"state": "issued",
234+
"account_number": account_number,
223235
}
224236
)
225-
if payment.partner_id.bank_ids:
226-
payment.account_number = payment.partner_id.bank_ids[0].acc_number
227-
else:
228-
payment.account_number = None
229237

230-
if not payments:
231-
payments = payment
232-
else:
233-
payments += payment
234-
if create_batch:
235-
if i % max_batch_size == 0:
236-
curr_batch = self.env["spp.payment.batch"].create(
237-
{
238-
"name": str(uuid4()),
239-
"cycle_id": cycle.id,
240-
"stats_datetime": fields.Datetime.now(),
241-
"tag_id": batch_tag.id,
242-
}
243-
)
244-
if not batches:
245-
batches = curr_batch
246-
else:
247-
batches += curr_batch
248-
curr_batch.payment_ids = [(4, payment.id)]
249-
payment.batch_id = curr_batch
238+
# Batch create all payments for this tag
239+
tag_payments = self.env["spp.payment"].create(payment_vals_list)
240+
241+
if not payments:
242+
payments = tag_payments
243+
else:
244+
payments += tag_payments
245+
246+
if create_batch:
247+
# Assign payments to batches in chunks of max_batch_size
248+
for i in range(0, len(tag_payments), max_batch_size):
249+
batch_payments = tag_payments[i : i + max_batch_size]
250+
curr_batch = self.env["spp.payment.batch"].create(
251+
{
252+
"name": str(uuid4()),
253+
"cycle_id": cycle.id,
254+
"stats_datetime": fields.Datetime.now(),
255+
"tag_id": batch_tag.id,
256+
}
257+
)
258+
batch_payments.write({"batch_id": curr_batch.id})
259+
if not batches:
260+
batches = curr_batch
261+
else:
262+
batches += curr_batch
250263
return payments, batches
251264

252265
def _prepare_payments_async(self, cycle, entitlements, entitlements_count):

spp_programs/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@
2222
from . import test_composite_indexes
2323
from . import test_cycle_computed_fields
2424
from . import test_fund_balance
25+
from . import test_batch_creation
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2+
import uuid
3+
from unittest.mock import patch
4+
5+
from odoo import fields
6+
from odoo.tests import TransactionCase
7+
8+
9+
class TestBatchEntitlementCreation(TransactionCase):
10+
"""Test that cash entitlement manager creates entitlements in a single
11+
batch call instead of one-by-one."""
12+
13+
def setUp(self):
14+
super().setUp()
15+
self.program = self.env["spp.program"].create({"name": f"Test Program {uuid.uuid4().hex[:8]}"})
16+
self.journal = self.env["account.journal"].create(
17+
{
18+
"name": "Test Journal",
19+
"type": "bank",
20+
"code": f"TJ{uuid.uuid4().hex[:4].upper()}",
21+
}
22+
)
23+
self.program.journal_id = self.journal.id
24+
25+
self.cycle = self.env["spp.cycle"].create(
26+
{
27+
"name": "Test Cycle",
28+
"program_id": self.program.id,
29+
"start_date": fields.Date.today(),
30+
"end_date": fields.Date.today(),
31+
}
32+
)
33+
self.manager = self.env["spp.program.entitlement.manager.cash"].create(
34+
{
35+
"name": "Test Cash Manager",
36+
"program_id": self.program.id,
37+
}
38+
)
39+
self.env["spp.program.entitlement.manager.cash.item"].create(
40+
{
41+
"entitlement_id": self.manager.id,
42+
"amount": 100.0,
43+
}
44+
)
45+
46+
# Create beneficiaries with cycle memberships
47+
self.registrants = self.env["res.partner"]
48+
self.memberships = self.env["spp.cycle.membership"]
49+
for i in range(5):
50+
reg = self.env["res.partner"].create({"name": f"Registrant {i}", "is_registrant": True})
51+
self.registrants |= reg
52+
self.memberships |= self.env["spp.cycle.membership"].create(
53+
{
54+
"partner_id": reg.id,
55+
"cycle_id": self.cycle.id,
56+
"state": "enrolled",
57+
}
58+
)
59+
60+
def test_cash_manager_batch_creates_entitlements(self):
61+
"""Cash entitlement manager must call create() at most once
62+
(batch), not once per beneficiary."""
63+
original_create = type(self.env["spp.entitlement"]).create
64+
65+
call_count = 0
66+
total_created = 0
67+
68+
def counting_create(self_model, vals_list):
69+
nonlocal call_count, total_created
70+
call_count += 1
71+
if isinstance(vals_list, list):
72+
total_created += len(vals_list)
73+
else:
74+
total_created += 1
75+
return original_create(self_model, vals_list)
76+
77+
with patch.object(
78+
type(self.env["spp.entitlement"]),
79+
"create",
80+
counting_create,
81+
):
82+
self.manager.prepare_entitlements(self.cycle, self.memberships)
83+
84+
self.assertEqual(
85+
call_count,
86+
1,
87+
f"create() should be called once (batch), was called {call_count} times",
88+
)
89+
self.assertEqual(total_created, 5)
90+
91+
92+
class TestBatchPaymentCreation(TransactionCase):
93+
"""Test that payment manager creates payments in a single batch call
94+
instead of one-by-one."""
95+
96+
def setUp(self):
97+
super().setUp()
98+
self.program = self.env["spp.program"].create({"name": f"Test Program {uuid.uuid4().hex[:8]}"})
99+
self.journal = self.env["account.journal"].create(
100+
{
101+
"name": "Test Journal",
102+
"type": "bank",
103+
"code": f"TJ{uuid.uuid4().hex[:4].upper()}",
104+
}
105+
)
106+
self.program.journal_id = self.journal.id
107+
108+
self.cycle = self.env["spp.cycle"].create(
109+
{
110+
"name": "Test Cycle",
111+
"program_id": self.program.id,
112+
"start_date": fields.Date.today(),
113+
"end_date": fields.Date.today(),
114+
}
115+
)
116+
self.payment_manager = self.env["spp.program.payment.manager.default"].create(
117+
{
118+
"name": "Test Payment Manager",
119+
"program_id": self.program.id,
120+
"create_batch": False,
121+
}
122+
)
123+
124+
# Create approved entitlements
125+
self.entitlements = self.env["spp.entitlement"]
126+
for i in range(5):
127+
reg = self.env["res.partner"].create({"name": f"Registrant {i}", "is_registrant": True})
128+
self.entitlements |= self.env["spp.entitlement"].create(
129+
{
130+
"partner_id": reg.id,
131+
"cycle_id": self.cycle.id,
132+
"initial_amount": 100.0,
133+
"state": "approved",
134+
"is_cash_entitlement": True,
135+
}
136+
)
137+
138+
def test_payment_manager_batch_creates_payments(self):
139+
"""Payment manager must call create() at most once per batch tag
140+
(batch), not once per entitlement."""
141+
original_create = type(self.env["spp.payment"]).create
142+
143+
call_count = 0
144+
145+
def counting_create(self_model, vals_list):
146+
nonlocal call_count
147+
call_count += 1
148+
return original_create(self_model, vals_list)
149+
150+
# Add a batch tag so we enter the loop
151+
batch_tag = self.env["spp.payment.batch.tag"].create(
152+
{
153+
"name": "Test Tag",
154+
"order": 1,
155+
"domain": "[]",
156+
"max_batch_size": 500,
157+
}
158+
)
159+
self.payment_manager.batch_tag_ids = [(4, batch_tag.id)]
160+
161+
with patch.object(
162+
type(self.env["spp.payment"]),
163+
"create",
164+
counting_create,
165+
):
166+
self.payment_manager._prepare_payments(self.cycle, self.entitlements)
167+
168+
self.assertEqual(
169+
call_count,
170+
1,
171+
f"create() should be called once (batch), was called {call_count} times",
172+
)

0 commit comments

Comments
 (0)