diff --git a/hr_course/__manifest__.py b/hr_course/__manifest__.py
index c85b95a452f..0443f0fe2a7 100644
--- a/hr_course/__manifest__.py
+++ b/hr_course/__manifest__.py
@@ -13,6 +13,7 @@
"data": [
"security/course_security.xml",
"security/ir.model.access.csv",
+ "data/hr_course_sequence.xml",
"views/hr_course_category_views.xml",
"views/hr_course_views.xml",
"views/hr_course_schedule_views.xml",
diff --git a/hr_course/data/hr_course_sequence.xml b/hr_course/data/hr_course_sequence.xml
new file mode 100644
index 00000000000..235fefd6960
--- /dev/null
+++ b/hr_course/data/hr_course_sequence.xml
@@ -0,0 +1,20 @@
+
+
+
+
+ Course
+ hr.course
+ C
+ 5
+
+
+
+
+ Course Schedule
+ hr.course.schedule
+ CS
+ 5
+
+
+
diff --git a/hr_course/models/hr_course.py b/hr_course/models/hr_course.py
index 75ebed14c66..1f2ce122a6f 100644
--- a/hr_course/models/hr_course.py
+++ b/hr_course/models/hr_course.py
@@ -37,6 +37,7 @@ class HrCourse(models.Model):
_inherit = ["mail.thread", "mail.activity.mixin"]
name = fields.Char(required=True, tracking=True)
+ code = fields.Char(string="Reference", copy=False, index="btree_not_null")
category_id = fields.Many2one(
"hr.course.category", string="Category", required=True
)
@@ -53,6 +54,37 @@ class HrCourse(models.Model):
"hr.course.schedule", inverse_name="course_id"
)
+ _sql_constraints = [
+ ("hr_course_code_uniq", "unique (code)", "The reference must be unique!"),
+ ]
+
+ @api.depends("code", "name")
+ def _compute_display_name(self):
+ for record in self:
+ if record.code and record.name:
+ record.display_name = f"[{record.code}] {record.name}"
+ elif record.name:
+ record.display_name = record.name
+ else:
+ record.display_name = record.code or ""
+
+ @api.model
+ def name_search(self, name, args=None, operator="ilike", limit=100):
+ args = args or []
+ recs = self.search([("code", operator, name)] + args, limit=limit)
+ if not recs.ids:
+ return super().name_search(
+ name=name, args=args, operator=operator, limit=limit
+ )
+ return [(r.id, r.display_name) for r in recs]
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ for vals in vals_list:
+ if not vals.get("code"):
+ vals["code"] = self.env["ir.sequence"].next_by_code("hr.course") or ""
+ return super().create(vals_list)
+
@api.onchange("permanence")
def _onchange_permanence(self):
self.permanence_time = False
diff --git a/hr_course/models/hr_course_schedule.py b/hr_course/models/hr_course_schedule.py
index 641002c4dc8..0410fc62617 100644
--- a/hr_course/models/hr_course_schedule.py
+++ b/hr_course/models/hr_course_schedule.py
@@ -9,6 +9,7 @@ class HrCourseSchedule(models.Model):
_description = "Course Schedule"
_inherit = ["mail.thread", "mail.activity.mixin"]
name = fields.Char(required=True, tracking=True)
+ code = fields.Char(string="Reference", copy=False, index="btree_not_null")
course_id = fields.Many2one("hr.course", string="Course", required=True)
start_date = fields.Date(
@@ -54,6 +55,43 @@ class HrCourseSchedule(models.Model):
)
note = fields.Text()
+ _sql_constraints = [
+ (
+ "hr_course_schedule_code_uniq",
+ "unique (code)",
+ "The reference must be unique!",
+ ),
+ ]
+
+ @api.depends("code", "name")
+ def _compute_display_name(self):
+ for record in self:
+ if record.code and record.name:
+ record.display_name = f"[{record.code}] {record.name}"
+ elif record.name:
+ record.display_name = record.name
+ else:
+ record.display_name = record.code or ""
+
+ @api.model
+ def name_search(self, name, args=None, operator="ilike", limit=100):
+ args = args or []
+ recs = self.search([("code", operator, name)] + args, limit=limit)
+ if not recs.ids:
+ return super().name_search(
+ name=name, args=args, operator=operator, limit=limit
+ )
+ return [(r.id, r.display_name) for r in recs]
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ for vals in vals_list:
+ if not vals.get("code"):
+ vals["code"] = (
+ self.env["ir.sequence"].next_by_code("hr.course.schedule") or ""
+ )
+ return super().create(vals_list)
+
@api.constrains("start_date", "end_date")
def _check_start_end_dates(self):
self.ensure_one()
diff --git a/hr_course/tests/test_hr_course.py b/hr_course/tests/test_hr_course.py
index b692a1cfb51..25507a8cdc4 100644
--- a/hr_course/tests/test_hr_course.py
+++ b/hr_course/tests/test_hr_course.py
@@ -1,6 +1,8 @@
# Copyright 2019 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+import psycopg2
+
import odoo.tests.common as common
from odoo.exceptions import ValidationError
@@ -37,6 +39,170 @@ def test_hr_course(self):
self.course_id._onchange_permanence()
self.assertFalse(self.course_id.permanence_time)
+ def test_hr_course_code_auto(self):
+ course = self.env["hr.course"].create(
+ {
+ "name": "Course with code",
+ "category_id": self.course_categ.id,
+ }
+ )
+ self.assertTrue(course.code)
+ self.assertTrue(course.code.startswith("C"))
+
+ def test_hr_course_schedule_code_auto(self):
+ schedule = self.env["hr.course.schedule"].create(
+ {
+ "name": "Schedule with code",
+ "course_id": self.course_id.id,
+ "cost": 100,
+ "authorized_by": self.employee1.id,
+ }
+ )
+ self.assertTrue(schedule.code)
+ self.assertTrue(schedule.code.startswith("CS"))
+
+ def test_hr_course_code_unique(self):
+ self.env["hr.course"].create(
+ {
+ "name": "Course 1",
+ "category_id": self.course_categ.id,
+ "code": "UNIQUE-CODE",
+ }
+ )
+ with self.assertRaises(psycopg2.IntegrityError):
+ self.env["hr.course"].create(
+ {
+ "name": "Course 2",
+ "category_id": self.course_categ.id,
+ "code": "UNIQUE-CODE",
+ }
+ )
+
+ def test_hr_course_name_search_by_code(self):
+ course = self.env["hr.course"].create(
+ {
+ "name": "Searchable Course",
+ "category_id": self.course_categ.id,
+ "code": "SEARCH-123",
+ }
+ )
+ result = self.env["hr.course"].name_search("SEARCH-123")
+ self.assertIn(course.id, [r[0] for r in result])
+
+ def test_hr_course_name_search_fallback(self):
+ course = self.env["hr.course"].create(
+ {
+ "name": "Fallback Course",
+ "category_id": self.course_categ.id,
+ "code": "FALL-001",
+ }
+ )
+ result = self.env["hr.course"].name_search("Fallback Course")
+ self.assertIn(course.id, [r[0] for r in result])
+
+ def test_hr_course_display_name(self):
+ course_both = self.env["hr.course"].create(
+ {
+ "name": "Named Course",
+ "category_id": self.course_categ.id,
+ "code": "CODE-1",
+ }
+ )
+ self.assertEqual(course_both.display_name, "[CODE-1] Named Course")
+
+ course_name_only = self.env["hr.course"].create(
+ {
+ "name": "Name Only",
+ "category_id": self.course_categ.id,
+ }
+ )
+ course_name_only.code = False
+ self.assertEqual(course_name_only.display_name, "Name Only")
+
+ def test_hr_course_explicit_code(self):
+ course = self.env["hr.course"].create(
+ {
+ "name": "Explicit Code",
+ "category_id": self.course_categ.id,
+ "code": "MY-CODE",
+ }
+ )
+ self.assertEqual(course.code, "MY-CODE")
+
+ def test_hr_course_schedule_display_name(self):
+ schedule_both = self.env["hr.course.schedule"].create(
+ {
+ "name": "Named Schedule",
+ "course_id": self.course_id.id,
+ "cost": 100,
+ "authorized_by": self.employee1.id,
+ "code": "SCH-1",
+ }
+ )
+ self.assertEqual(schedule_both.display_name, "[SCH-1] Named Schedule")
+
+ schedule_name_only = self.env["hr.course.schedule"].create(
+ {
+ "name": "Name Only Schedule",
+ "course_id": self.course_id.id,
+ "cost": 100,
+ "authorized_by": self.employee1.id,
+ }
+ )
+ schedule_name_only.code = False
+ self.assertEqual(schedule_name_only.display_name, "Name Only Schedule")
+
+ def test_hr_course_schedule_name_search(self):
+ schedule = self.env["hr.course.schedule"].create(
+ {
+ "name": "Searchable Schedule",
+ "course_id": self.course_id.id,
+ "cost": 100,
+ "authorized_by": self.employee1.id,
+ "code": "SCH-SEARCH",
+ }
+ )
+ result = self.env["hr.course.schedule"].name_search("SCH-SEARCH")
+ self.assertIn(schedule.id, [r[0] for r in result])
+
+ result_fallback = self.env["hr.course.schedule"].name_search(
+ "Searchable Schedule"
+ )
+ self.assertIn(schedule.id, [r[0] for r in result_fallback])
+
+ def test_hr_course_schedule_code_unique(self):
+ self.env["hr.course.schedule"].create(
+ {
+ "name": "Schedule 1",
+ "course_id": self.course_id.id,
+ "cost": 100,
+ "authorized_by": self.employee1.id,
+ "code": "UNIQUE-SCH",
+ }
+ )
+ with self.assertRaises(psycopg2.IntegrityError):
+ self.env["hr.course.schedule"].create(
+ {
+ "name": "Schedule 2",
+ "course_id": self.course_id.id,
+ "cost": 100,
+ "authorized_by": self.employee1.id,
+ "code": "UNIQUE-SCH",
+ }
+ )
+
+ def test_hr_course_schedule_explicit_code(self):
+ schedule = self.env["hr.course.schedule"].create(
+ {
+ "name": "Explicit Schedule",
+ "course_id": self.course_id.id,
+ "cost": 100,
+ "authorized_by": self.employee1.id,
+ "code": "MY-SCH-CODE",
+ }
+ )
+ self.assertEqual(schedule.code, "MY-SCH-CODE")
+
def test_hr_course_schedule(self):
with self.assertRaises(ValidationError):
self.course_schedule_id.write({"end_date": "2019-02-10"})
diff --git a/hr_course/views/hr_course_schedule_views.xml b/hr_course/views/hr_course_schedule_views.xml
index 403ed34e4a3..54e1866d967 100644
--- a/hr_course/views/hr_course_schedule_views.xml
+++ b/hr_course/views/hr_course_schedule_views.xml
@@ -85,6 +85,7 @@
+
+
@@ -171,6 +173,7 @@
decoration-success="state=='completed'"
decoration-muted="state=='cancelled'"
>
+
diff --git a/hr_course/views/hr_course_views.xml b/hr_course/views/hr_course_views.xml
index aeae171f0ab..bc76b7d2631 100644
--- a/hr_course/views/hr_course_views.xml
+++ b/hr_course/views/hr_course_views.xml
@@ -15,6 +15,7 @@
+
@@ -65,6 +66,7 @@
hr.course
+