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
7 changes: 5 additions & 2 deletions spp_programs/models/managers/entitlement_manager_cash.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,16 @@ def prepare_entitlements(self, cycle, beneficiaries):
if addl_fields:
new_entitlements_to_create[beneficiary_id.id].update(addl_fields)

# Create entitlement records
# Create entitlement records in a single batch
vals_list = []
for ent in new_entitlements_to_create:
initial_amount = new_entitlements_to_create[ent]["initial_amount"]
new_entitlements_to_create[ent]["initial_amount"] = self._check_subsidy(initial_amount)
# Create non-zero entitlements only
if new_entitlements_to_create[ent]["initial_amount"] > 0.0:
self.env["spp.entitlement"].create(new_entitlements_to_create[ent])
vals_list.append(new_entitlements_to_create[ent])
Comment on lines 161 to +166

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While the logic is correct, the loop can be made more direct and Pythonic by iterating over the dictionary's values instead of its keys. This improves readability by removing the extra dictionary lookup new_entitlements_to_create[ent] in each iteration.

Suggested change
for ent in new_entitlements_to_create:
initial_amount = new_entitlements_to_create[ent]["initial_amount"]
new_entitlements_to_create[ent]["initial_amount"] = self._check_subsidy(initial_amount)
# Create non-zero entitlements only
if new_entitlements_to_create[ent]["initial_amount"] > 0.0:
self.env["spp.entitlement"].create(new_entitlements_to_create[ent])
vals_list.append(new_entitlements_to_create[ent])
for vals in new_entitlements_to_create.values():
initial_amount = vals["initial_amount"]
vals["initial_amount"] = self._check_subsidy(initial_amount)
# Create non-zero entitlements only
if vals["initial_amount"] > 0.0:
vals_list.append(vals)

if vals_list:
self.env["spp.entitlement"].create(vals_list)

def _get_addl_entitlement_fields(self, beneficiary_id):
"""
Expand Down
65 changes: 39 additions & 26 deletions spp_programs/models/managers/payment_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,42 +211,55 @@ def _prepare_payments(self, cycle, entitlements):
entitlements -= tag_entitlements
max_batch_size = batch_tag.max_batch_size

for i, entitlement_id in enumerate(tag_entitlements):
payment = self.env["spp.payment"].create(
if not tag_entitlements:
continue

# Prefetch bank_ids to avoid N+1 queries
tag_entitlements.mapped("partner_id.bank_ids.acc_number")

# Build all payment vals in one pass
payment_vals_list = []
for entitlement_id in tag_entitlements:
account_number = None
if entitlement_id.partner_id.bank_ids:
account_number = entitlement_id.partner_id.bank_ids[0].acc_number
payment_vals_list.append(
{
"name": str(uuid4()),
"entitlement_id": entitlement_id.id,
"cycle_id": entitlement_id.cycle_id.id,
"amount_issued": entitlement_id.initial_amount,
"payment_fee": entitlement_id.transfer_fee,
"state": "issued",
"account_number": account_number,
}
)
Comment on lines +221 to 236

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for building payment_vals_list can be made more concise and Pythonic by using a list comprehension. This improves readability and reduces boilerplate code.

            payment_vals_list = [
                {
                    "name": str(uuid4()),
                    "entitlement_id": ent.id,
                    "cycle_id": ent.cycle_id.id,
                    "amount_issued": ent.initial_amount,
                    "payment_fee": ent.transfer_fee,
                    "state": "issued",
                    "account_number": ent.partner_id.bank_ids[0].acc_number if ent.partner_id.bank_ids else None,
                }
                for ent in tag_entitlements
            ]

if payment.partner_id.bank_ids:
payment.account_number = payment.partner_id.bank_ids[0].acc_number
else:
payment.account_number = None

if not payments:
payments = payment
else:
payments += payment
if create_batch:
if i % max_batch_size == 0:
curr_batch = self.env["spp.payment.batch"].create(
{
"name": str(uuid4()),
"cycle_id": cycle.id,
"stats_datetime": fields.Datetime.now(),
"tag_id": batch_tag.id,
}
)
if not batches:
batches = curr_batch
else:
batches += curr_batch
curr_batch.payment_ids = [(4, payment.id)]
payment.batch_id = curr_batch
# Batch create all payments for this tag
tag_payments = self.env["spp.payment"].create(payment_vals_list)

if not payments:
payments = tag_payments
else:
payments += tag_payments

if create_batch:
# Assign payments to batches in chunks of max_batch_size
for i in range(0, len(tag_payments), max_batch_size):
batch_payments = tag_payments[i : i + max_batch_size]
curr_batch = self.env["spp.payment.batch"].create(
{
"name": str(uuid4()),
"cycle_id": cycle.id,
"stats_datetime": fields.Datetime.now(),
"tag_id": batch_tag.id,
}
)
batch_payments.write({"batch_id": curr_batch.id})
if not batches:
batches = curr_batch
else:
batches += curr_batch
return payments, batches

def _prepare_payments_async(self, cycle, entitlements, entitlements_count):
Expand Down
1 change: 1 addition & 0 deletions spp_programs/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@
from . import test_eligibility_cel_integration
from . import test_compliance_cel
from . import test_create_program_wizard_cel
from . import test_batch_creation
172 changes: 172 additions & 0 deletions spp_programs/tests/test_batch_creation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
import uuid
from unittest.mock import patch

from odoo import fields
from odoo.tests import TransactionCase


class TestBatchEntitlementCreation(TransactionCase):
"""Test that cash entitlement manager creates entitlements in a single
batch call instead of one-by-one."""

def setUp(self):
super().setUp()
self.program = self.env["spp.program"].create({"name": f"Test Program {uuid.uuid4().hex[:8]}"})
self.journal = self.env["account.journal"].create(
{
"name": "Test Journal",
"type": "bank",
"code": f"TJ{uuid.uuid4().hex[:4].upper()}",
}
)
self.program.journal_id = self.journal.id

self.cycle = self.env["spp.cycle"].create(
{
"name": "Test Cycle",
"program_id": self.program.id,
"start_date": fields.Date.today(),
"end_date": fields.Date.today(),
}
)
self.manager = self.env["spp.program.entitlement.manager.cash"].create(
{
"name": "Test Cash Manager",
"program_id": self.program.id,
}
)
self.env["spp.program.entitlement.manager.cash.item"].create(
{
"entitlement_id": self.manager.id,
"amount": 100.0,
}
)

# Create beneficiaries with cycle memberships
self.registrants = self.env["res.partner"]
self.memberships = self.env["spp.cycle.membership"]
for i in range(5):
reg = self.env["res.partner"].create({"name": f"Registrant {i}", "is_registrant": True})
self.registrants |= reg
self.memberships |= self.env["spp.cycle.membership"].create(
{
"partner_id": reg.id,
"cycle_id": self.cycle.id,
"state": "enrolled",
}
)

def test_cash_manager_batch_creates_entitlements(self):
"""Cash entitlement manager must call create() at most once
(batch), not once per beneficiary."""
original_create = type(self.env["spp.entitlement"]).create

call_count = 0
total_created = 0

def counting_create(self_model, vals_list):
nonlocal call_count, total_created
call_count += 1
if isinstance(vals_list, list):
total_created += len(vals_list)
else:
total_created += 1
return original_create(self_model, vals_list)

with patch.object(
type(self.env["spp.entitlement"]),
"create",
counting_create,
):
self.manager.prepare_entitlements(self.cycle, self.memberships)

self.assertEqual(
call_count,
1,
f"create() should be called once (batch), was called {call_count} times",
)
self.assertEqual(total_created, 5)


class TestBatchPaymentCreation(TransactionCase):
"""Test that payment manager creates payments in a single batch call
instead of one-by-one."""

def setUp(self):
super().setUp()
self.program = self.env["spp.program"].create({"name": f"Test Program {uuid.uuid4().hex[:8]}"})
self.journal = self.env["account.journal"].create(
{
"name": "Test Journal",
"type": "bank",
"code": f"TJ{uuid.uuid4().hex[:4].upper()}",
}
)
self.program.journal_id = self.journal.id

self.cycle = self.env["spp.cycle"].create(
{
"name": "Test Cycle",
"program_id": self.program.id,
"start_date": fields.Date.today(),
"end_date": fields.Date.today(),
}
)
self.payment_manager = self.env["spp.program.payment.manager.default"].create(
{
"name": "Test Payment Manager",
"program_id": self.program.id,
"create_batch": False,
}
)

# Create approved entitlements
self.entitlements = self.env["spp.entitlement"]
for i in range(5):
reg = self.env["res.partner"].create({"name": f"Registrant {i}", "is_registrant": True})
self.entitlements |= self.env["spp.entitlement"].create(
{
"partner_id": reg.id,
"cycle_id": self.cycle.id,
"initial_amount": 100.0,
"state": "approved",
"is_cash_entitlement": True,
}
)

def test_payment_manager_batch_creates_payments(self):
"""Payment manager must call create() at most once per batch tag
(batch), not once per entitlement."""
original_create = type(self.env["spp.payment"]).create

call_count = 0

def counting_create(self_model, vals_list):
nonlocal call_count
call_count += 1
return original_create(self_model, vals_list)

# Add a batch tag so we enter the loop
batch_tag = self.env["spp.payment.batch.tag"].create(
{
"name": "Test Tag",
"order": 1,
"domain": "[]",
"max_batch_size": 500,
}
)
self.payment_manager.batch_tag_ids = [(4, batch_tag.id)]

with patch.object(
type(self.env["spp.payment"]),
"create",
counting_create,
):
self.payment_manager._prepare_payments(self.cycle, self.entitlements)

self.assertEqual(
call_count,
1,
f"create() should be called once (batch), was called {call_count} times",
)
Comment on lines +143 to +172

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better test coverage and consistency with test_cash_manager_batch_creates_entitlements, this test should also verify the total number of records created, not just the number of create() calls. This ensures that the batch operation not only runs once but also processes the correct number of items.

        call_count = 0
        total_created = 0

        def counting_create(self_model, vals_list):
            nonlocal call_count, total_created
            call_count += 1
            if isinstance(vals_list, list):
                total_created += len(vals_list)
            else:
                total_created += 1
            return original_create(self_model, vals_list)

        # Add a batch tag so we enter the loop
        batch_tag = self.env["spp.payment.batch.tag"].create(
            {
                "name": "Test Tag",
                "order": 1,
                "domain": "[]",
                "max_batch_size": 500,
            }
        )
        self.payment_manager.batch_tag_ids = [(4, batch_tag.id)]

        with patch.object(
            type(self.env["spp.payment"]),
            "create",
            counting_create,
        ):
            self.payment_manager._prepare_payments(self.cycle, self.entitlements)

        self.assertEqual(
            call_count,
            1,
            f"create() should be called once (batch), was called {call_count} times",
        )
        self.assertEqual(total_created, 5)

Loading