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 +