Skip to content
Open
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
1 change: 1 addition & 0 deletions hr_course/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 20 additions & 0 deletions hr_course/data/hr_course_sequence.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2024 Miquel Rosell
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo noupdate="1">
<record id="seq_hr_course" model="ir.sequence">
<field name="name">Course</field>
<field name="code">hr.course</field>
<field name="prefix">C</field>
<field name="padding">5</field>
<field name="company_id" eval="False" />
</record>

<record id="seq_hr_course_schedule" model="ir.sequence">
<field name="name">Course Schedule</field>
<field name="code">hr.course.schedule</field>
<field name="prefix">CS</field>
<field name="padding">5</field>
<field name="company_id" eval="False" />
</record>
</odoo>
32 changes: 32 additions & 0 deletions hr_course/models/hr_course.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand All @@ -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
Expand Down
38 changes: 38 additions & 0 deletions hr_course/models/hr_course_schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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()
Expand Down
166 changes: 166 additions & 0 deletions hr_course/tests/test_hr_course.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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"})
Expand Down
3 changes: 3 additions & 0 deletions hr_course/views/hr_course_schedule_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
</div>
<group>
<group>
<field name="code" />
<field name="course_id" />
<field
name="cost"
Expand Down Expand Up @@ -159,6 +160,7 @@
<field name="arch" type="xml">
<search>
<field name="name" string="Course Name" />
<field name="code" />
<field name="course_id" string="Course" />
</search>
</field>
Expand All @@ -171,6 +173,7 @@
decoration-success="state=='completed'"
decoration-muted="state=='cancelled'"
>
<field name="code" optional="show" />
<field name="name" />
<field name="start_date" />
<field name="end_date" />
Expand Down
2 changes: 2 additions & 0 deletions hr_course/views/hr_course_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
</div>
<group>
<group>
<field name="code" />
<field name="category_id" />
<field name="permanence" />
<field name="permanence_time" invisible="not permanence" />
Expand Down Expand Up @@ -65,6 +66,7 @@
<field name="model">hr.course</field>
<field name="arch" type="xml">
<tree>
<field name="code" optional="show" />
<field name="name" />
<field name="category_id" />
<field name="content" />
Expand Down
Loading