Skip to content

Commit 95434f7

Browse files
authored
Merge pull request #848 from OpenSPP/phone-validity
Phone Validation
2 parents c9e56c9 + 5c99104 commit 95434f7

9 files changed

Lines changed: 346 additions & 2 deletions

File tree

spp_base_common/__manifest__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@
2222
],
2323
"excludes": [],
2424
"external_dependencies": {},
25-
"data": ["security/security_access.xml", "security/ir.model.access.csv", "views/main_view.xml"],
25+
"data": [
26+
"security/security_access.xml",
27+
"security/ir.model.access.csv",
28+
"views/main_view.xml",
29+
"views/phone_validation_view.xml",
30+
],
2631
"assets": {
2732
"web.assets_backend": [
2833
"spp_base_common/static/src/scss/navbar.scss",

spp_base_common/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
from . import ir_module_module
2+
from . import phone_validation
3+
from . import phone_number
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import logging
2+
import re
3+
4+
from odoo import _, api, models
5+
from odoo.exceptions import ValidationError
6+
7+
_logger = logging.getLogger(__name__)
8+
9+
10+
class G2PPhoneNumber(models.Model):
11+
_inherit = "g2p.phone.number"
12+
13+
def write(self, vals):
14+
res = super().write(vals)
15+
if "phone_no" in vals or "country_id" in vals:
16+
self._onchange_phone_validation()
17+
return res
18+
19+
@api.model_create_multi
20+
def create(self, vals):
21+
record = super().create(vals)
22+
record._onchange_phone_validation()
23+
return record
24+
25+
@api.onchange("phone_no", "country_id")
26+
def _onchange_phone_validation(self):
27+
for rec in self:
28+
phone_validation = rec.env["spp.phone.validation"].search([("state", "=", "active")])
29+
if not rec.phone_no:
30+
return
31+
32+
phone_no = rec.phone_no
33+
# Remove spaces, parentheses, and dashes for validation purposes
34+
phone_no = phone_no.replace(" ", "").replace("(", "").replace(")", "").replace("-", "")
35+
36+
error_msgs = []
37+
38+
# Check for letters
39+
has_letter = bool(re.search(r"[A-Za-z]", phone_no))
40+
if has_letter:
41+
error_msgs.append(_("Phone number must not contain letters."))
42+
43+
# Check for invalid special characters (allow only digits, '-', and '+' at the start)
44+
# Acceptable pattern: optional leading '+', digits and '-' only
45+
# Only add special character error if there are non-letter invalid characters
46+
if re.search(r"[^0-9A-Za-z\-+]", phone_no) or not re.match(r"^\+?[\d-]+$", phone_no):
47+
# Only add if there is at least one invalid special character (not a letter)
48+
# Find all invalid special characters
49+
invalid_chars = re.findall(r"[^0-9A-Za-z\-+]", phone_no)
50+
if invalid_chars:
51+
error_msgs.append(
52+
_(
53+
"Phone number contains invalid special characters. "
54+
"Only digits, '-', and an optional leading '+' are allowed."
55+
)
56+
)
57+
58+
# Format validation
59+
if phone_validation:
60+
format_msgs = []
61+
validated_success_count = 0
62+
for validation in phone_validation:
63+
if validation.with_prefix:
64+
pattern = (
65+
r"^\+?" + re.escape(validation.prefix) + r"\d{" + str(validation.number_of_digits) + r"}$"
66+
)
67+
else:
68+
pattern = r"^\d{" + str(validation.number_of_digits) + r"}$"
69+
if re.match(pattern, phone_no):
70+
validated_success_count += 1
71+
else:
72+
format_msgs.append(validation.name)
73+
74+
if validated_success_count == 0 and not error_msgs:
75+
error_msgs.append(
76+
_("Phone number must match one of the following formats: ") + ", ".join(format_msgs)
77+
)
78+
79+
if error_msgs:
80+
raise ValidationError("\n".join(error_msgs))
81+
return
82+
83+
@api.depends("phone_no", "country_id")
84+
def _compute_phone_sanitized(self):
85+
for rec in self:
86+
phone_no = rec.phone_no
87+
phone_no = phone_no.replace(" ", "").replace("(", "").replace(")", "").replace("-", "") if phone_no else ""
88+
rec.phone_sanitized = phone_no
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from odoo import api, fields, models
2+
3+
4+
class SPPPhoneValidation(models.Model):
5+
_name = "spp.phone.validation"
6+
_description = "SPP Phone Validation"
7+
8+
name = fields.Char(string="Sample Format", compute="_compute_name")
9+
number_of_digits = fields.Integer(string="Number of Digits", required=True)
10+
with_prefix = fields.Boolean(string="With Prefix")
11+
prefix = fields.Char(string="Prefix")
12+
state = fields.Selection(
13+
[("active", "Active"), ("inactive", "Inactive")],
14+
string="State",
15+
default="active",
16+
)
17+
active = fields.Boolean(string="Active", default=True)
18+
19+
@api.depends("number_of_digits", "with_prefix", "prefix")
20+
def _compute_name(self):
21+
for record in self:
22+
if record.with_prefix and record.prefix:
23+
record.name = f"{record.prefix}{'X'*record.number_of_digits}"
24+
else:
25+
record.name = "X" * record.number_of_digits
26+
27+
def activate_phone_validation(self):
28+
for record in self:
29+
record.state = "active"
30+
31+
def deactivate_phone_validation(self):
32+
for record in self:
33+
record.state = "inactive"

spp_base_common/security/ir.model.access.csv

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ registrant_tag_read_registry_access,Registrant Tags Read Access,g2p_registry_bas
88
id_type_read_registry_access,ID Type Read Access,g2p_registry_base.model_g2p_id_type,spp_base_common.read_registry,1,0,0,0
99
spp_read_res_model_access,Res Model Read Access,base.model_ir_model,spp_base_common.read_registry,1,0,0,0
1010
spp_read_res_model_fields_access,Res Model Fields Read Access,base.model_ir_model_fields,spp_base_common.read_registry,1,0,0,0
11+
spp_phone_number_validation_read_access,Phone Number Validation Read Access,spp_base_common.model_spp_phone_validation,spp_base_common.read_registry,1,0,0,0
1112

1213
res_partner_write_registry_access,Registry Write Access,base.model_res_partner,spp_base_common.write_registry,1,1,0,0
1314
phone_number_write_registry_access,Phone Number Write Access,g2p_registry_base.model_g2p_phone_number,spp_base_common.write_registry,1,1,0,0
@@ -17,6 +18,7 @@ registrant_tag_write_registry_access,Registrant Tags Write Access,g2p_registry_b
1718
id_type_write_registry_access,ID Type Write Access,g2p_registry_base.model_g2p_id_type,spp_base_common.write_registry,1,1,0,0
1819
spp_write_res_model_access,Res Model Write Access,base.model_ir_model,spp_base_common.write_registry,1,1,0,0
1920
spp_write_res_model_fields_access,Res Model Fields Write Access,base.model_ir_model_fields,spp_base_common.write_registry,1,1,0,0
21+
spp_phone_number_validation_write_access,Phone Number Validation Write Access,spp_base_common.model_spp_phone_validation,spp_base_common.write_registry,1,1,0,0
2022

2123
res_partner_create_registry_access,Registry Create Access,base.model_res_partner,spp_base_common.create_registry,1,0,1,0
2224
phone_number_create_registry_access,Phone Number Create Access,g2p_registry_base.model_g2p_phone_number,spp_base_common.create_registry,1,0,1,0
@@ -26,3 +28,4 @@ registrant_tag_create_registry_access,Registrant Tags Create Access,g2p_registry
2628
id_type_create_registry_access,ID Type Create Access,g2p_registry_base.model_g2p_id_type,spp_base_common.create_registry,1,0,1,0
2729
spp_create_res_model_access,Res Model Create Access,base.model_ir_model,spp_base_common.create_registry,1,0,1,0
2830
spp_create_res_model_fields_access,Res Model Fields Create Access,base.model_ir_model_fields,spp_base_common.create_registry,1,0,1,0
31+
spp_phone_number_validation_create_access,Phone Number Validation Create Access,spp_base_common.model_spp_phone_validation,spp_base_common.create_registry,1,0,1,0

spp_base_common/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from . import test_ir_module_module
2+
from . import test_phone_number_validation
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from odoo.exceptions import ValidationError
2+
from odoo.tests import TransactionCase
3+
4+
5+
class TestPhoneValidation(TransactionCase):
6+
@classmethod
7+
def setUpClass(cls):
8+
super().setUpClass()
9+
cls.partner_model = cls.env["res.partner"]
10+
cls.phone_validation_model = cls.env["spp.phone.validation"]
11+
cls.phone_model = cls.env["g2p.phone.number"]
12+
cls.registrant = cls.partner_model.create(
13+
{
14+
"name": "Test Registrant",
15+
"is_registrant": True,
16+
}
17+
)
18+
cls.phone_validation_1 = cls.phone_validation_model.create(
19+
{
20+
"number_of_digits": 10,
21+
"with_prefix": True,
22+
"prefix": "+63",
23+
"state": "active",
24+
}
25+
)
26+
cls.phone_validation_2 = cls.phone_validation_model.create(
27+
{
28+
"number_of_digits": 11,
29+
"with_prefix": False,
30+
"state": "active",
31+
}
32+
)
33+
34+
def test_01_create_phone_with_invalid_number(self):
35+
phone = self.phone_model.create(
36+
{
37+
"partner_id": self.registrant.id,
38+
"phone_no": "+639123456789",
39+
"country_id": self.env.ref("base.ph").id,
40+
}
41+
)
42+
43+
with self.assertRaises(ValidationError) as cm:
44+
phone.phone_no = "12345"
45+
phone._onchange_phone_validation()
46+
47+
self.assertIn("Phone number must match one of the following formats", str(cm.exception))
48+
49+
def test_02_create_phone_with_valid_number_with_prefix(self):
50+
phone = self.phone_model.create(
51+
{
52+
"partner_id": self.registrant.id,
53+
"phone_no": "+639123456789",
54+
"country_id": self.env.ref("base.ph").id,
55+
}
56+
)
57+
phone._onchange_phone_validation()
58+
self.assertEqual(phone.phone_no, "+639123456789")
59+
60+
phone = self.phone_model.create(
61+
{
62+
"partner_id": self.registrant.id,
63+
"phone_no": "+639-1234-56789",
64+
"country_id": self.env.ref("base.ph").id,
65+
}
66+
)
67+
phone._onchange_phone_validation()
68+
self.assertEqual(phone.phone_no, "+639-1234-56789")
69+
self.assertEqual(phone.phone_sanitized, "+639123456789")
70+
71+
def test_03_create_phone_with_valid_number_without_prefix(self):
72+
phone = self.phone_model.create(
73+
{
74+
"partner_id": self.registrant.id,
75+
"phone_no": "09123456789",
76+
"country_id": self.env.ref("base.ph").id,
77+
}
78+
)
79+
phone._onchange_phone_validation()
80+
self.assertEqual(phone.phone_no, "09123456789")
81+
82+
def test_04_create_phone_with_letters_in_number(self):
83+
phone_vals = {
84+
"partner_id": self.registrant.id,
85+
"phone_no": "09123A56789",
86+
"country_id": self.env.ref("base.ph").id,
87+
}
88+
with self.assertRaises(ValidationError) as cm:
89+
self.phone_model.create(phone_vals)
90+
91+
self.assertIn("Phone number must not contain letters", str(cm.exception))
92+
93+
def test_05_create_phone_with_invalid_special_characters(self):
94+
phone_vals = {
95+
"partner_id": self.registrant.id,
96+
"phone_no": "09123@456789",
97+
"country_id": self.env.ref("base.ph").id,
98+
}
99+
100+
with self.assertRaises(ValidationError) as cm:
101+
self.phone_model.create(phone_vals)
102+
103+
self.assertIn("Phone number contains invalid special characters", str(cm.exception))
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<!--
3+
Part of OpenSPP Registry. See LICENSE file for full copyright and licensing details.
4+
-->
5+
<odoo>
6+
<record id="view_phone_validation_tree" model="ir.ui.view">
7+
<field name="name">view_phone_validation_tree</field>
8+
<field name="model">spp.phone.validation</field>
9+
<field name="priority">1</field>
10+
<field name="arch" type="xml">
11+
<tree create="true" edit="true" delete="false" duplicate="false" editable="top">
12+
<field name="number_of_digits" />
13+
<field name="with_prefix" />
14+
<field name="prefix" />
15+
<field name="name" />
16+
<field
17+
name="state"
18+
decoration-danger="state == 'inactive'"
19+
decoration-success="state == 'active'"
20+
readonly="1"
21+
/>
22+
<button
23+
name="activate_phone_validation"
24+
title="Activate"
25+
type="object"
26+
icon="fa-check"
27+
class="btn-success"
28+
invisible="state == 'active'"
29+
/>
30+
<button
31+
name="deactivate_phone_validation"
32+
title="Deactivate"
33+
type="object"
34+
icon="fa-times"
35+
class="btn-danger"
36+
invisible="state == 'inactive'"
37+
/>
38+
<field name="active" invisible="1" />
39+
</tree>
40+
</field>
41+
</record>
42+
43+
<record id="phone_validation_search_view" model="ir.ui.view">
44+
<field name="name">phone.validation.search.view</field>
45+
<field name="model">spp.phone.validation</field>
46+
<field name="arch" type="xml">
47+
<search>
48+
<filter
49+
string="Active"
50+
name="active"
51+
domain="[('state', '=', 'active')]"
52+
help="Filter to show only active phone validations."
53+
/>
54+
<filter
55+
string="Inactive"
56+
name="active"
57+
domain="[('state', '=', 'inactive')]"
58+
help="Filter to show only inactive phone validations."
59+
/>
60+
<filter
61+
string="Archived"
62+
name="archived"
63+
domain="[('active', '=', False)]"
64+
help="Filter to show only archived phone validations."
65+
/>
66+
</search>
67+
</field>
68+
</record>
69+
70+
<record id="action_phone_validation" model="ir.actions.act_window">
71+
<field name="name">Phone Validation</field>
72+
<field name="type">ir.actions.act_window</field>
73+
<field name="res_model">spp.phone.validation</field>
74+
<field name="view_mode">tree</field>
75+
<field name="context">{}</field>
76+
<field name="domain">[]</field>
77+
<field name="search_view_id" ref="phone_validation_search_view" />
78+
<field name="help" type="html">
79+
<p class="o_view_nocontent_smiling_face">
80+
Add a Phone Validation!
81+
</p><p>
82+
Click the create button to enter a phone validation.
83+
</p>
84+
</field>
85+
</record>
86+
87+
<record id="action_phone_validation_tree_view" model="ir.actions.act_window.view">
88+
<field name="sequence" eval="1" />
89+
<field name="view_mode">tree</field>
90+
<field name="view_id" ref="view_phone_validation_tree" />
91+
<field name="act_window_id" ref="action_phone_validation" />
92+
</record>
93+
94+
<menuitem
95+
id="menu_phone_validation"
96+
name="Phone Validation"
97+
action="action_phone_validation"
98+
parent="g2p_registry_base.g2p_configuration_menu_root"
99+
sequence="10"
100+
groups="g2p_registry_base.group_g2p_admin,g2p_registry_base.group_g2p_registrar"
101+
/>
102+
103+
</odoo>

0 commit comments

Comments
 (0)