Skip to content
Closed
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
8 changes: 7 additions & 1 deletion spp_change_request/security/change_request_security.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,45 @@
<record id="group_spp_change_request_agent" model="res.groups">
<field name="name">Change Request Agent</field>
<field name="category_id" ref="spp_change_request_base.spp_change_request_module" />
<field name="implied_ids" eval="[(4, ref('spp_registry_base.read_registry'))]" />
</record>

<record id="group_spp_change_request_validator" model="res.groups">
<field name="name">Change Request Validator</field>
<field name="category_id" ref="spp_change_request_base.spp_change_request_module" />
<field name="implied_ids" eval="[(4, ref('spp_registry_base.read_registry'))]" />
</record>

<record id="group_spp_change_request_applicator" model="res.groups">
<field name="name">Change Request Applicator</field>
<field name="category_id" ref="spp_change_request_base.spp_change_request_module" />
<field name="implied_ids" eval="[(4, ref('spp_registry_base.read_registry'))]" />
</record>

<record id="group_spp_change_request_administrator" model="res.groups">
<field name="name">Change Request Administrator</field>
<field name="category_id" ref="spp_change_request_base.spp_change_request_module" />
<field name="implied_ids" eval="[(4, ref('spp_registry_base.read_registry'))]" />
</record>

<record id="group_spp_change_request_hq_validator" model="res.groups">
<field name="name">Change Request Validator HQ</field>
<field name="category_id" ref="spp_change_request_base.spp_change_request_module" />
<field name="implied_ids" eval="[(4, ref('spp_registry_base.read_registry'))]" />
</record>

<record id="group_spp_change_request_external_api" model="res.groups">
<field name="name">Change Request External API</field>
<field name="category_id" ref="spp_change_request_base.spp_change_request_module" />
<field name="implied_ids" eval="[(4, ref('spp_registry_base.read_registry'))]" />
</record>

<record id="group_spp_change_request_local_validator" model="res.groups">
<field name="name">Change Request Validator Local</field>
<field name="category_id" ref="spp_change_request_base.spp_change_request_module" />
<field
name="implied_ids"
eval="[(4, ref('spp_change_request.group_spp_change_request_validator'))]"
eval="[(4, ref('spp_change_request.group_spp_change_request_validator')), (4, ref('spp_registry_base.read_registry'))]"
/>
</record>

Expand Down
1 change: 1 addition & 0 deletions spp_registry_base/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"views/groups_view.xml",
"views/individuals_view.xml",
"views/main_view.xml",
"views/phone_validation_view.xml",
],
"assets": {
"web.assets_backend": [
Expand Down
2 changes: 2 additions & 0 deletions spp_registry_base/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from . import res_partner
from . import phone_number
from . import phone_number_validation
48 changes: 48 additions & 0 deletions spp_registry_base/models/phone_number.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import logging
import re

from odoo import _, api, models
from odoo.exceptions import ValidationError

_logger = logging.getLogger(__name__)


class G2PPhoneNumber(models.Model):
_inherit = "g2p.phone.number"

def write(self, vals):
res = super().write(vals)
if "phone_no" in vals or "country_id" in vals:
self._onchange_phone_validation()
return res

@api.model_create_multi
def create(self, vals):
record = super().create(vals)
record._onchange_phone_validation()
return record
Comment on lines +19 to +23

Choose a reason for hiding this comment

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

high

The create method is decorated with @api.model_create_multi but is defined as an instance method (def create(self, vals):). According to Odoo conventions, it should be a class method. This is confusing and can lead to unexpected behavior. Please refactor it to use the standard signature def create(cls, vals_list): and conventional variable names.

Suggested change
@api.model_create_multi
def create(self, vals):
record = super().create(vals)
record._onchange_phone_validation()
return record
@api.model_create_multi
def create(cls, vals_list):
records = super().create(vals_list)
records._onchange_phone_validation()
return records


@api.onchange("phone_no", "country_id")
def _onchange_phone_validation(self):
phone_validation = self.env["spp.phone.validation"].search([("state", "=", "active")])
if not self.phone_no:
return

phone_no = self.phone_no
if phone_validation:
validated_success_count = 0
error_msg = []
for validation in phone_validation:
if validation.with_prefix:
pattern = r"^\+?" + re.escape(validation.prefix) + r"\d{" + str(validation.number_of_digits) + r"}$"
else:
pattern = r"^\d{" + str(validation.number_of_digits) + r"}$"
if re.match(pattern, phone_no):
validated_success_count += 1
else:
error_msg.append(validation.name)

if validated_success_count == 0:
message = "Phone number must match one of the following formats: " + ", ".join(error_msg)
raise ValidationError(_(message))
return
Comment on lines +26 to +48

Choose a reason for hiding this comment

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

critical

This method is called from create and write, which can operate on multiple records at once. However, the current implementation does not handle multi-recordsets (self). For example, self.phone_no will raise an error if self contains more than one record. The entire logic should be wrapped in a loop to iterate over each record in self.

Additionally, the validation logic can be made more efficient. Instead of iterating through all validations and counting successes, you can stop at the first successful match. If no match is found after checking all validations, then you can raise the error.

    def _onchange_phone_validation(self):
        phone_validation = self.env["spp.phone.validation"].search([("state", "=", "active")])
        if not phone_validation:
            return

        for record in self:
            if not record.phone_no:
                continue

            phone_no = record.phone_no

            is_valid = False
            for validation in phone_validation:
                if validation.with_prefix:
                    pattern = r"^\+?" + re.escape(validation.prefix) + r"\d{" + str(validation.number_of_digits) + r"}$"
                else:
                    pattern = r"^\d{" + str(validation.number_of_digits) + r"}$"
                if re.match(pattern, phone_no):
                    is_valid = True
                    break

            if not is_valid:
                error_msg = [v.name for v in phone_validation]
                message = "Phone number must match one of the following formats: " + ", ".join(error_msg)
                raise ValidationError(_(message))
        return

32 changes: 32 additions & 0 deletions spp_registry_base/models/phone_number_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from odoo import api, fields, models


class SPPPhoneValidation(models.Model):
_name = "spp.phone.validation"
_description = "SPP Phone Validation"

name = fields.Char(string="Sample Format", compute="_compute_name")
number_of_digits = fields.Integer(string="Number of Digits", required=True)
with_prefix = fields.Boolean(string="With Prefix")
prefix = fields.Char(string="Prefix")
state = fields.Selection(
[("active", "Active"), ("inactive", "Inactive")],
string="State",
default="active",
)

@api.depends("number_of_digits", "with_prefix", "prefix")
def _compute_name(self):
for record in self:
if record.with_prefix and record.prefix:
record.name = f"{record.prefix}{'X'*record.number_of_digits}"
else:
record.name = "X" * record.number_of_digits

def activate_phone_validation(self):
for record in self:
record.state = "active"

def deactivate_phone_validation(self):
for record in self:
record.state = "inactive"
3 changes: 3 additions & 0 deletions spp_registry_base/security/ir.model.access.csv
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ id_type_read_registry_access,ID Type Read Access,g2p_registry_base.model_g2p_id_
group_kind_read_registry_access,Group Kind Read Access,g2p_registry_group.model_g2p_group_kind,spp_registry_base.read_registry,1,0,0,0
spp_read_res_model_access,Res Model Read Access,base.model_ir_model,spp_registry_base.read_registry,1,0,0,0
spp_read_res_model_fields_access,Res Model Fields Read Access,base.model_ir_model_fields,spp_registry_base.read_registry,1,0,0,0
spp_phone_number_validation_read_access,Phone Number Validation Read Access,spp_registry_base.model_spp_phone_validation,spp_registry_base.read_registry,1,0,0,0

res_partner_write_registry_access,Registry Write Access,base.model_res_partner,spp_registry_base.write_registry,1,1,0,0
group_membership_write_registry_access,Group Membership Write Access,g2p_registry_membership.model_g2p_group_membership,spp_registry_base.write_registry,1,1,0,0
Expand All @@ -21,6 +22,7 @@ id_type_write_registry_access,ID Type Write Access,g2p_registry_base.model_g2p_i
group_kind_write_registry_access,Group Kind Write Access,g2p_registry_group.model_g2p_group_kind,spp_registry_base.write_registry,1,1,0,0
spp_write_res_model_access,Res Model Write Access,base.model_ir_model,spp_registry_base.write_registry,1,1,0,0
spp_write_res_model_fields_access,Res Model Fields Write Access,base.model_ir_model_fields,spp_registry_base.write_registry,1,1,0,0
spp_phone_number_validation_write_access,Phone Number Validation Write Access,spp_registry_base.model_spp_phone_validation,spp_registry_base.write_registry,1,1,0,0

res_partner_create_registry_access,Registry Create Access,base.model_res_partner,spp_registry_base.create_registry,1,0,1,0
group_membership_create_registry_access,Group Membership Create Access,g2p_registry_membership.model_g2p_group_membership,spp_registry_base.create_registry,1,1,1,0
Expand All @@ -32,3 +34,4 @@ id_type_create_registry_access,ID Type Create Access,g2p_registry_base.model_g2p
group_kind_create_registry_access,Group Kind Create Access,g2p_registry_group.model_g2p_group_kind,spp_registry_base.create_registry,1,0,1,0
spp_create_res_model_access,Res Model Create Access,base.model_ir_model,spp_registry_base.create_registry,1,0,1,0
spp_create_res_model_fields_access,Res Model Fields Create Access,base.model_ir_model_fields,spp_registry_base.create_registry,1,0,1,0
spp_phone_number_validation_create_access,Phone Number Validation Create Access,spp_registry_base.model_spp_phone_validation,spp_registry_base.create_registry,1,0,1,0
1 change: 1 addition & 0 deletions spp_registry_base/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from . import test_res_partner
from . import test_phone_number_validation
69 changes: 69 additions & 0 deletions spp_registry_base/tests/test_phone_number_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from odoo.exceptions import ValidationError
from odoo.tests import TransactionCase


class TestPhoneValidation(TransactionCase):

Choose a reason for hiding this comment

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

medium

The current test suite is a good start, but it could be improved to cover more cases:

  1. Directly test create and write failures: Instead of manually calling _onchange_phone_validation, you should test that create() and write() raise a ValidationError when provided with invalid phone numbers. This will properly test the ORM method overrides.
  2. Test multi-record operations: The identified issue with multi-recordset handling in _onchange_phone_validation was not caught because the tests only operate on single records. Please add tests for creating and writing multiple g2p.phone.number records at once.

Example for point 1:

# Test create
with self.assertRaises(ValidationError):
    self.phone_model.create({
        "partner_id": self.registrant.id,
        "phone_no": "12345",
        "country_id": self.env.ref("base.ph").id,
    })

# Test write
phone = self.phone_model.create(...) # with valid data
with self.assertRaises(ValidationError):
    phone.write({"phone_no": "12345"})

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.partner_model = cls.env["res.partner"]
cls.phone_validation_model = cls.env["spp.phone.validation"]
cls.phone_model = cls.env["g2p.phone.number"]
cls.registrant = cls.partner_model.create(
{
"name": "Test Registrant",
"is_registrant": True,
}
)
cls.phone_validation_1 = cls.phone_validation_model.create(
{
"number_of_digits": 10,
"with_prefix": True,
"prefix": "+63",
"state": "active",
}
)
cls.phone_validation_2 = cls.phone_validation_model.create(
{
"number_of_digits": 11,
"with_prefix": False,
"state": "active",
}
)

def test_01_create_phone_with_invalid_number(self):
phone = self.phone_model.create(
{
"partner_id": self.registrant.id,
"phone_no": "+639123456789",
"country_id": self.env.ref("base.ph").id,
}
)

with self.assertRaises(ValidationError) as cm:
phone.phone_no = "12345"
phone._onchange_phone_validation()

self.assertIn("Phone number must match one of the following formats", str(cm.exception))

def test_02_create_phone_with_valid_number_with_prefix(self):
phone = self.phone_model.create(
{
"partner_id": self.registrant.id,
"phone_no": "+639123456789",
"country_id": self.env.ref("base.ph").id,
}
)
phone._onchange_phone_validation()
self.assertEqual(phone.phone_no, "+639123456789")

def test_03_create_phone_with_valid_number_without_prefix(self):
phone = self.phone_model.create(
{
"partner_id": self.registrant.id,
"phone_no": "09123456789",
"country_id": self.env.ref("base.ph").id,
}
)
phone._onchange_phone_validation()
self.assertEqual(phone.phone_no, "09123456789")
74 changes: 74 additions & 0 deletions spp_registry_base/views/phone_validation_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Part of OpenSPP Registry. See LICENSE file for full copyright and licensing details.
-->
<odoo>
<record id="view_phone_validation_tree" model="ir.ui.view">
<field name="name">view_phone_validation_tree</field>
<field name="model">spp.phone.validation</field>
<field name="priority">1</field>
<field name="arch" type="xml">
<tree create="true" edit="true" delete="false" editable="top">
<field name="number_of_digits" />
<field name="with_prefix" />
<field name="prefix" />
<field name="name" />

Choose a reason for hiding this comment

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

medium

The name field is computed by the _compute_name method and is not stored in the database. In an editable tree view (editable="top"), making a non-stored computed field editable can be confusing for the user, as any changes they make will be lost. It's better to make it readonly.

Suggested change
<field name="name" />
<field name="name" readonly="1" />

<field
name="state"
decoration-danger="state == 'inactive'"
decoration-success="state == 'active'"
readonly="1"
/>
<button
name="activate_phone_validation"
title="Activate"
type="object"
icon="fa-check"
class="btn-success"
invisible="state == 'active'"
/>
<button
name="deactivate_phone_validation"
title="Deactivate"
type="object"
icon="fa-times"
class="btn-danger"
invisible="state == 'inactive'"
/>
</tree>
</field>
</record>

<record id="action_phone_validation" model="ir.actions.act_window">
<field name="name">Phone Validation</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">spp.phone.validation</field>
<field name="view_mode">tree</field>
<field name="context">{}</field>
<field name="domain">[]</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Add a Phone Validation!
</p><p>
Click the create button to enter a phone validation.
</p>
</field>
</record>

<record id="action_phone_validation_tree_view" model="ir.actions.act_window.view">
<field name="sequence" eval="1" />
<field name="view_mode">tree</field>
<field name="view_id" ref="view_phone_validation_tree" />
<field name="act_window_id" ref="action_phone_validation" />
</record>

<menuitem
id="menu_phone_validation"
name="Phone Validation"
action="action_phone_validation"
parent="g2p_registry_base.g2p_configuration_menu_root"
sequence="10"
groups="g2p_registry_base.group_g2p_admin,g2p_registry_base.group_g2p_registrar"
/>

</odoo>
Loading