Skip to content

Commit e69a504

Browse files
Merge pull request #116 from OpenSPP/ken/optimize_spp_program
perf(spp_programs): replace Python uniqueness checks with SQL constraints
2 parents 0eeb24a + 62345ad commit e69a504

12 files changed

Lines changed: 397 additions & 71 deletions

File tree

spp_programs/README.rst

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

257+
19.0.2.0.1
258+
~~~~~~~~~~
259+
260+
- Replace Python-level uniqueness checks with SQL UNIQUE constraints for
261+
program membership, cycle membership, and entitlement codes
262+
- Add pre-migration script to deduplicate existing data before
263+
constraint creation
264+
257265
19.0.2.0.0
258266
~~~~~~~~~~
259267

spp_programs/__manifest__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"name": "OpenSPP Programs",
55
"summary": "Manage cash and in-kind entitlements, integrate with inventory, and enhance program management features for comprehensive social protection and agricultural support.",
66
"category": "OpenSPP/Core",
7-
"version": "19.0.2.0.0",
7+
"version": "19.0.2.0.1",
88
"sequence": 1,
99
"author": "OpenSPP.org",
1010
"website": "https://github.com/OpenSPP/OpenSPP2",
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2+
import logging
3+
4+
_logger = logging.getLogger(__name__)
5+
6+
7+
def migrate(cr, version):
8+
"""Deduplicate data before adding SQL UNIQUE constraints.
9+
10+
Removes duplicate rows using ROW_NUMBER() OVER (PARTITION BY ...),
11+
keeping the earliest record (lowest id) for each unique combination.
12+
"""
13+
if not version:
14+
return
15+
16+
_deduplicate_program_memberships(cr)
17+
_deduplicate_cycle_memberships(cr)
18+
_deduplicate_entitlement_codes(cr)
19+
20+
21+
def _deduplicate_program_memberships(cr):
22+
"""Remove duplicate (partner_id, program_id) rows from spp_program_membership."""
23+
cr.execute(
24+
"""
25+
DELETE FROM spp_program_membership
26+
WHERE id IN (
27+
SELECT id FROM (
28+
SELECT id,
29+
ROW_NUMBER() OVER (
30+
PARTITION BY partner_id, program_id
31+
ORDER BY id
32+
) AS rn
33+
FROM spp_program_membership
34+
) sub
35+
WHERE rn > 1
36+
)
37+
"""
38+
)
39+
if cr.rowcount:
40+
_logger.info("Deduplicated %d duplicate program membership rows", cr.rowcount)
41+
42+
43+
def _deduplicate_cycle_memberships(cr):
44+
"""Remove duplicate (partner_id, cycle_id) rows from spp_cycle_membership."""
45+
cr.execute(
46+
"""
47+
DELETE FROM spp_cycle_membership
48+
WHERE id IN (
49+
SELECT id FROM (
50+
SELECT id,
51+
ROW_NUMBER() OVER (
52+
PARTITION BY partner_id, cycle_id
53+
ORDER BY id
54+
) AS rn
55+
FROM spp_cycle_membership
56+
) sub
57+
WHERE rn > 1
58+
)
59+
"""
60+
)
61+
if cr.rowcount:
62+
_logger.info("Deduplicated %d duplicate cycle membership rows", cr.rowcount)
63+
64+
65+
def _deduplicate_entitlement_codes(cr):
66+
"""Remove duplicate code values from spp_entitlement.
67+
68+
For duplicate codes, regenerates codes for the newer records rather
69+
than deleting them, since entitlements may have financial significance.
70+
"""
71+
cr.execute(
72+
"""
73+
UPDATE spp_entitlement
74+
SET code = code || '-' || id::text
75+
WHERE id IN (
76+
SELECT id FROM (
77+
SELECT id,
78+
ROW_NUMBER() OVER (
79+
PARTITION BY code
80+
ORDER BY id
81+
) AS rn
82+
FROM spp_entitlement
83+
WHERE code IS NOT NULL
84+
) sub
85+
WHERE rn > 1
86+
)
87+
"""
88+
)
89+
if cr.rowcount:
90+
_logger.info("Deduplicated %d entitlement rows with duplicate codes", cr.rowcount)

spp_programs/models/cycle_membership.py

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2-
from odoo import _, api, fields, models
2+
from odoo import _, fields, models
33
from odoo.exceptions import ValidationError
44

55

@@ -8,6 +8,11 @@ class SPPCycleMembership(models.Model):
88
_description = "Cycle Membership"
99
_order = "partner_id asc,id desc"
1010

11+
_unique_partner_cycle = models.Constraint(
12+
"UNIQUE(partner_id, cycle_id)",
13+
"Beneficiary must be unique per cycle.",
14+
)
15+
1116
partner_id = fields.Many2one("res.partner", "Registrant", help="A beneficiary", required=True, index=True)
1217
cycle_id = fields.Many2one("spp.cycle", "Cycle", help="A cycle", required=True, index=True)
1318
enrollment_date = fields.Date(default=lambda self: fields.Datetime.now())
@@ -24,25 +29,6 @@ class SPPCycleMembership(models.Model):
2429
copy=False,
2530
)
2631

27-
@api.constrains("partner_id", "cycle_id")
28-
def _check_unique_partner_per_cycle(self):
29-
# Prefetch partner_id and cycle_id to avoid N+1 queries in loop
30-
self.mapped("partner_id")
31-
self.mapped("cycle_id")
32-
33-
for record in self:
34-
if record.partner_id and record.cycle_id:
35-
existing = self.search(
36-
[
37-
("partner_id", "=", record.partner_id.id),
38-
("cycle_id", "=", record.cycle_id.id),
39-
("id", "!=", record.id),
40-
],
41-
limit=1,
42-
)
43-
if existing:
44-
raise ValidationError(_("Beneficiary must be unique per cycle."))
45-
4632
def _compute_display_name(self):
4733
res = super()._compute_display_name()
4834
# Prefetch cycle_id and partner_id to avoid N+1 queries in loop

spp_programs/models/entitlement.py

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ class SPPEntitlement(models.Model):
2626
_order = "partner_id asc,id desc"
2727
_check_company_auto = True
2828

29+
_unique_code = models.Constraint(
30+
"UNIQUE(code)",
31+
"Entitlement code must be unique.",
32+
)
33+
2934
# Cached model ID for performance (class-level cache)
3035
_entitlement_model_id_cache = None
3136

@@ -139,20 +144,6 @@ def _generate_code(self):
139144
payment_status = fields.Selection([("paid", "Paid"), ("notpaid", "Not Paid")], compute="_compute_payment_status")
140145
payment_date = fields.Date(compute="_compute_payment_status")
141146

142-
@api.constrains("code")
143-
def _check_unique_code(self):
144-
for record in self:
145-
if record.code:
146-
existing = self.search(
147-
[
148-
("code", "=", record.code),
149-
("id", "!=", record.id),
150-
],
151-
limit=1,
152-
)
153-
if existing:
154-
raise ValidationError(_("The entitlement code must be unique."))
155-
156147
@api.constrains("valid_from", "valid_until")
157148
def _check_valid_dates(self):
158149
for record in self:

spp_programs/models/entitlement_base_model.py

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ class SPPEntitlement(models.Model):
1717
_order = "partner_id asc,id desc"
1818
_check_company_auto = True
1919

20+
_unique_code = models.Constraint(
21+
"UNIQUE(code)",
22+
"Entitlement code must be unique.",
23+
)
24+
2025
@api.model
2126
def _generate_code(self):
2227
return str(uuid4())[4:-8][3:]
@@ -87,20 +92,6 @@ def _generate_code(self):
8792
payment_status = fields.Selection([("paid", "Paid"), ("notpaid", "Not Paid")], compute="_compute_payment_status")
8893
payment_date = fields.Date(compute="_compute_payment_status")
8994

90-
@api.constrains("code")
91-
def _check_unique_code(self):
92-
for record in self:
93-
if record.code:
94-
existing = self.search(
95-
[
96-
("code", "=", record.code),
97-
("id", "!=", record.id),
98-
],
99-
limit=1,
100-
)
101-
if existing:
102-
raise ValidationError(_("The entitlement code must be unique."))
103-
10495
@api.constrains("valid_from", "valid_until")
10596
def _check_valid_dates(self):
10697
for record in self:

spp_programs/models/program_membership.py

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from lxml import etree
44

55
from odoo import _, api, fields, models
6-
from odoo.exceptions import UserError, ValidationError
6+
from odoo.exceptions import UserError
77

88
from . import constants
99

@@ -19,6 +19,11 @@ class SPPProgramMembership(models.Model):
1919
_inherits = {"res.partner": "partner_id"}
2020
_order = "id desc"
2121

22+
_unique_partner_program = models.Constraint(
23+
"UNIQUE(partner_id, program_id)",
24+
"Beneficiary must be unique per program.",
25+
)
26+
2227
partner_id = fields.Many2one(
2328
"res.partner",
2429
"Registrant",
@@ -69,25 +74,6 @@ def _compute_duplicate_reason(self):
6974
else:
7075
rec.duplicate_reason = False
7176

72-
@api.constrains("partner_id", "program_id")
73-
def _check_unique_partner_per_program(self):
74-
# Prefetch partner_id and program_id to avoid N+1 queries in loop
75-
self.mapped("partner_id")
76-
self.mapped("program_id")
77-
78-
for record in self:
79-
if record.partner_id and record.program_id:
80-
existing = self.search(
81-
[
82-
("partner_id", "=", record.partner_id.id),
83-
("program_id", "=", record.program_id.id),
84-
("id", "!=", record.id),
85-
],
86-
limit=1,
87-
)
88-
if existing:
89-
raise ValidationError(_("Beneficiary must be unique per program."))
90-
9177
# TODO: Implement exit reasons
9278
# exit_reason_id = fields.Many2one("Exit Reason") Default: Completed, Opt-Out, Other
9379

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.1
2+
3+
- Replace Python-level uniqueness checks with SQL UNIQUE constraints for program membership, cycle membership, and entitlement codes
4+
- Add pre-migration script to deduplicate existing data before constraint creation
5+
16
### 19.0.2.0.0
27

38
- Initial migration to OpenSPP2

spp_programs/static/description/index.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,15 @@ <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.1</h1>
662+
<ul class="simple">
663+
<li>Replace Python-level uniqueness checks with SQL UNIQUE constraints for
664+
program membership, cycle membership, and entitlement codes</li>
665+
<li>Add pre-migration script to deduplicate existing data before
666+
constraint creation</li>
667+
</ul>
668+
</div>
669+
<div class="section" id="section-2">
661670
<h1>19.0.2.0.0</h1>
662671
<ul class="simple">
663672
<li>Initial migration to OpenSPP2</li>

spp_programs/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@
1717
from . import test_registrant
1818
from . import test_spp_cycle_compliance
1919
from . import test_spp_program_create_wizard_compliance
20+
from . import test_sql_constraints
2021
from . import test_stock_rule

0 commit comments

Comments
 (0)