Skip to content

Commit 7d505c6

Browse files
[IMP] hr_course: add reference code to courses and schedules
1 parent a5d0762 commit 7d505c6

7 files changed

Lines changed: 260 additions & 0 deletions

File tree

hr_course/__manifest__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"data": [
1414
"security/course_security.xml",
1515
"security/ir.model.access.csv",
16+
"data/hr_course_sequence.xml",
1617
"views/hr_course_category_views.xml",
1718
"views/hr_course_views.xml",
1819
"views/hr_course_schedule_views.xml",
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<!-- Copyright 2024 Miquel Rosell
3+
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
4+
<odoo noupdate="1">
5+
<record id="seq_hr_course" model="ir.sequence">
6+
<field name="name">Course</field>
7+
<field name="code">hr.course</field>
8+
<field name="prefix">C</field>
9+
<field name="padding">5</field>
10+
<field name="company_id" eval="False" />
11+
</record>
12+
13+
<record id="seq_hr_course_schedule" model="ir.sequence">
14+
<field name="name">Course Schedule</field>
15+
<field name="code">hr.course.schedule</field>
16+
<field name="prefix">CS</field>
17+
<field name="padding">5</field>
18+
<field name="company_id" eval="False" />
19+
</record>
20+
</odoo>

hr_course/models/hr_course.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class HrCourse(models.Model):
3737
_inherit = ["mail.thread", "mail.activity.mixin"]
3838

3939
name = fields.Char(required=True, tracking=True)
40+
code = fields.Char(string="Reference", copy=False, index="btree_not_null")
4041
category_id = fields.Many2one(
4142
"hr.course.category", string="Category", required=True
4243
)
@@ -53,6 +54,37 @@ class HrCourse(models.Model):
5354
"hr.course.schedule", inverse_name="course_id"
5455
)
5556

57+
_sql_constraints = [
58+
("hr_course_code_uniq", "unique (code)", "The reference must be unique!"),
59+
]
60+
61+
@api.depends("code", "name")
62+
def _compute_display_name(self):
63+
for record in self:
64+
if record.code and record.name:
65+
record.display_name = f"[{record.code}] {record.name}"
66+
elif record.name:
67+
record.display_name = record.name
68+
else:
69+
record.display_name = record.code or ""
70+
71+
@api.model
72+
def name_search(self, name, args=None, operator="ilike", limit=100):
73+
args = args or []
74+
recs = self.search([("code", operator, name)] + args, limit=limit)
75+
if not recs.ids:
76+
return super().name_search(
77+
name=name, args=args, operator=operator, limit=limit
78+
)
79+
return [(r.id, r.display_name) for r in recs]
80+
81+
@api.model_create_multi
82+
def create(self, vals_list):
83+
for vals in vals_list:
84+
if not vals.get("code"):
85+
vals["code"] = self.env["ir.sequence"].next_by_code("hr.course") or ""
86+
return super().create(vals_list)
87+
5688
@api.onchange("permanence")
5789
def _onchange_permanence(self):
5890
self.permanence_time = False

hr_course/models/hr_course_schedule.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class HrCourseSchedule(models.Model):
99
_description = "Course Schedule"
1010
_inherit = ["mail.thread", "mail.activity.mixin"]
1111
name = fields.Char(required=True, tracking=True)
12+
code = fields.Char(string="Reference", copy=False, index="btree_not_null")
1213
course_id = fields.Many2one("hr.course", string="Course", required=True)
1314

1415
start_date = fields.Date(
@@ -54,6 +55,43 @@ class HrCourseSchedule(models.Model):
5455
)
5556
note = fields.Text()
5657

58+
_sql_constraints = [
59+
(
60+
"hr_course_schedule_code_uniq",
61+
"unique (code)",
62+
"The reference must be unique!",
63+
),
64+
]
65+
66+
@api.depends("code", "name")
67+
def _compute_display_name(self):
68+
for record in self:
69+
if record.code and record.name:
70+
record.display_name = f"[{record.code}] {record.name}"
71+
elif record.name:
72+
record.display_name = record.name
73+
else:
74+
record.display_name = record.code or ""
75+
76+
@api.model
77+
def name_search(self, name, args=None, operator="ilike", limit=100):
78+
args = args or []
79+
recs = self.search([("code", operator, name)] + args, limit=limit)
80+
if not recs.ids:
81+
return super().name_search(
82+
name=name, args=args, operator=operator, limit=limit
83+
)
84+
return [(r.id, r.display_name) for r in recs]
85+
86+
@api.model_create_multi
87+
def create(self, vals_list):
88+
for vals in vals_list:
89+
if not vals.get("code"):
90+
vals["code"] = (
91+
self.env["ir.sequence"].next_by_code("hr.course.schedule") or ""
92+
)
93+
return super().create(vals_list)
94+
5795
@api.constrains("start_date", "end_date")
5896
def _check_start_end_dates(self):
5997
self.ensure_one()

hr_course/tests/test_hr_course.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Copyright 2019 Creu Blanca
22
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
33

4+
import psycopg2
5+
46
import odoo.tests.common as common
57
from odoo.exceptions import ValidationError
68

@@ -37,6 +39,168 @@ def test_hr_course(self):
3739
self.course_id._onchange_permanence()
3840
self.assertFalse(self.course_id.permanence_time)
3941

42+
def test_hr_course_code_auto(self):
43+
course = self.env["hr.course"].create(
44+
{
45+
"name": "Course with code",
46+
"category_id": self.course_categ.id,
47+
}
48+
)
49+
self.assertTrue(course.code)
50+
self.assertTrue(course.code.startswith("C"))
51+
52+
def test_hr_course_schedule_code_auto(self):
53+
schedule = self.env["hr.course.schedule"].create(
54+
{
55+
"name": "Schedule with code",
56+
"course_id": self.course_id.id,
57+
"cost": 100,
58+
"authorized_by": self.employee1.id,
59+
}
60+
)
61+
self.assertTrue(schedule.code)
62+
self.assertTrue(schedule.code.startswith("CS"))
63+
64+
def test_hr_course_code_unique(self):
65+
self.env["hr.course"].create(
66+
{
67+
"name": "Course 1",
68+
"category_id": self.course_categ.id,
69+
"code": "UNIQUE-CODE",
70+
}
71+
)
72+
with self.assertRaises(psycopg2.IntegrityError):
73+
self.env["hr.course"].create(
74+
{
75+
"name": "Course 2",
76+
"category_id": self.course_categ.id,
77+
"code": "UNIQUE-CODE",
78+
}
79+
)
80+
81+
def test_hr_course_name_search_by_code(self):
82+
course = self.env["hr.course"].create(
83+
{
84+
"name": "Searchable Course",
85+
"category_id": self.course_categ.id,
86+
"code": "SEARCH-123",
87+
}
88+
)
89+
result = self.env["hr.course"].name_search("SEARCH-123")
90+
self.assertIn(course.id, [r[0] for r in result])
91+
92+
def test_hr_course_name_search_fallback(self):
93+
course = self.env["hr.course"].create(
94+
{
95+
"name": "Fallback Course",
96+
"category_id": self.course_categ.id,
97+
"code": "FALL-001",
98+
}
99+
)
100+
result = self.env["hr.course"].name_search("Fallback Course")
101+
self.assertIn(course.id, [r[0] for r in result])
102+
103+
def test_hr_course_display_name(self):
104+
course_both = self.env["hr.course"].create(
105+
{
106+
"name": "Named Course",
107+
"category_id": self.course_categ.id,
108+
"code": "CODE-1",
109+
}
110+
)
111+
self.assertEqual(course_both.display_name, "[CODE-1] Named Course")
112+
113+
course_name_only = self.env["hr.course"].create(
114+
{
115+
"name": "Name Only",
116+
"category_id": self.course_categ.id,
117+
}
118+
)
119+
self.assertEqual(course_name_only.display_name, "Name Only")
120+
121+
def test_hr_course_explicit_code(self):
122+
course = self.env["hr.course"].create(
123+
{
124+
"name": "Explicit Code",
125+
"category_id": self.course_categ.id,
126+
"code": "MY-CODE",
127+
}
128+
)
129+
self.assertEqual(course.code, "MY-CODE")
130+
131+
def test_hr_course_schedule_display_name(self):
132+
schedule_both = self.env["hr.course.schedule"].create(
133+
{
134+
"name": "Named Schedule",
135+
"course_id": self.course_id.id,
136+
"cost": 100,
137+
"authorized_by": self.employee1.id,
138+
"code": "SCH-1",
139+
}
140+
)
141+
self.assertEqual(schedule_both.display_name, "[SCH-1] Named Schedule")
142+
143+
schedule_name_only = self.env["hr.course.schedule"].create(
144+
{
145+
"name": "Name Only Schedule",
146+
"course_id": self.course_id.id,
147+
"cost": 100,
148+
"authorized_by": self.employee1.id,
149+
}
150+
)
151+
self.assertEqual(schedule_name_only.display_name, "Name Only Schedule")
152+
153+
def test_hr_course_schedule_name_search(self):
154+
schedule = self.env["hr.course.schedule"].create(
155+
{
156+
"name": "Searchable Schedule",
157+
"course_id": self.course_id.id,
158+
"cost": 100,
159+
"authorized_by": self.employee1.id,
160+
"code": "SCH-SEARCH",
161+
}
162+
)
163+
result = self.env["hr.course.schedule"].name_search("SCH-SEARCH")
164+
self.assertIn(schedule.id, [r[0] for r in result])
165+
166+
result_fallback = self.env["hr.course.schedule"].name_search(
167+
"Searchable Schedule"
168+
)
169+
self.assertIn(schedule.id, [r[0] for r in result_fallback])
170+
171+
def test_hr_course_schedule_code_unique(self):
172+
self.env["hr.course.schedule"].create(
173+
{
174+
"name": "Schedule 1",
175+
"course_id": self.course_id.id,
176+
"cost": 100,
177+
"authorized_by": self.employee1.id,
178+
"code": "UNIQUE-SCH",
179+
}
180+
)
181+
with self.assertRaises(psycopg2.IntegrityError):
182+
self.env["hr.course.schedule"].create(
183+
{
184+
"name": "Schedule 2",
185+
"course_id": self.course_id.id,
186+
"cost": 100,
187+
"authorized_by": self.employee1.id,
188+
"code": "UNIQUE-SCH",
189+
}
190+
)
191+
192+
def test_hr_course_schedule_explicit_code(self):
193+
schedule = self.env["hr.course.schedule"].create(
194+
{
195+
"name": "Explicit Schedule",
196+
"course_id": self.course_id.id,
197+
"cost": 100,
198+
"authorized_by": self.employee1.id,
199+
"code": "MY-SCH-CODE",
200+
}
201+
)
202+
self.assertEqual(schedule.code, "MY-SCH-CODE")
203+
40204
def test_hr_course_schedule(self):
41205
with self.assertRaises(ValidationError):
42206
self.course_schedule_id.write({"end_date": "2019-02-10"})

hr_course/views/hr_course_schedule_views.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
</div>
8686
<group>
8787
<group>
88+
<field name="code" />
8889
<field name="course_id" />
8990
<field
9091
name="cost"
@@ -159,6 +160,7 @@
159160
<field name="arch" type="xml">
160161
<search>
161162
<field name="name" string="Course Name" />
163+
<field name="code" />
162164
<field name="course_id" string="Course" />
163165
</search>
164166
</field>
@@ -171,6 +173,7 @@
171173
decoration-success="state=='completed'"
172174
decoration-muted="state=='cancelled'"
173175
>
176+
<field name="code" optional="show" />
174177
<field name="name" />
175178
<field name="start_date" />
176179
<field name="end_date" />

hr_course/views/hr_course_views.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
</div>
1616
<group>
1717
<group>
18+
<field name="code" />
1819
<field name="category_id" />
1920
<field name="permanence" />
2021
<field name="permanence_time" invisible="not permanence" />
@@ -65,6 +66,7 @@
6566
<field name="model">hr.course</field>
6667
<field name="arch" type="xml">
6768
<tree>
69+
<field name="code" optional="show" />
6870
<field name="name" />
6971
<field name="category_id" />
7072
<field name="content" />

0 commit comments

Comments
 (0)