From 87ac2897f322cf3a1a78e4d819c4091c067e78a6 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 18 Sep 2020 11:28:39 -0700 Subject: [PATCH] [NEW] hr_attendance_work_entry: for Odoo 13 --- hr_attendance_work_entry/__init__.py | 15 +++++ hr_attendance_work_entry/__manifest__.py | 17 ++++++ .../data/hr_attendance_work_entry_data.xml | 17 ++++++ hr_attendance_work_entry/models/__init__.py | 3 + hr_attendance_work_entry/models/attendance.py | 9 +++ hr_attendance_work_entry/models/employee.py | 58 +++++++++++++++++++ hr_attendance_work_entry/models/work_entry.py | 13 +++++ hr_attendance_work_entry/tests/__init__.py | 1 + .../tests/test_attendance_work_type.py | 47 +++++++++++++++ 9 files changed, 180 insertions(+) create mode 100644 hr_attendance_work_entry/__init__.py create mode 100755 hr_attendance_work_entry/__manifest__.py create mode 100644 hr_attendance_work_entry/data/hr_attendance_work_entry_data.xml create mode 100644 hr_attendance_work_entry/models/__init__.py create mode 100644 hr_attendance_work_entry/models/attendance.py create mode 100644 hr_attendance_work_entry/models/employee.py create mode 100644 hr_attendance_work_entry/models/work_entry.py create mode 100644 hr_attendance_work_entry/tests/__init__.py create mode 100644 hr_attendance_work_entry/tests/test_attendance_work_type.py diff --git a/hr_attendance_work_entry/__init__.py b/hr_attendance_work_entry/__init__.py new file mode 100644 index 00000000..26c033a8 --- /dev/null +++ b/hr_attendance_work_entry/__init__.py @@ -0,0 +1,15 @@ +from . import models + + +def attn_payroll_pre_init_hook(cr): + """ + This module installs a Work Entry Type with code "ATTN" + If you have undergone a migration (either for this module + or even your own Payslip Work Entry lines with code "ATTN") + then the uniqueness constraint will prevent this module + from installing. + """ + cr.execute("UPDATE hr_work_entry_type " + "SET code = 'ATTN-PRE-INSTALL' " + "WHERE code = 'ATTN';" + ) diff --git a/hr_attendance_work_entry/__manifest__.py b/hr_attendance_work_entry/__manifest__.py new file mode 100755 index 00000000..97d1d7ab --- /dev/null +++ b/hr_attendance_work_entry/__manifest__.py @@ -0,0 +1,17 @@ +{ + 'name': 'Attendance Work Entry Type', + 'description': 'Set work types on attendance records.', + 'version': '13.0.1.0.0', + 'website': 'https://hibou.io/', + 'author': 'Hibou Corp. ', + 'license': 'AGPL-3', + 'category': 'Human Resources', + 'data': [ + 'data/hr_attendance_work_entry_data.xml', + ], + 'depends': [ + 'hr_attendance', + 'hr_work_entry', + ], + 'pre_init_hook': 'attn_payroll_pre_init_hook', +} diff --git a/hr_attendance_work_entry/data/hr_attendance_work_entry_data.xml b/hr_attendance_work_entry/data/hr_attendance_work_entry_data.xml new file mode 100644 index 00000000..ab99a1ca --- /dev/null +++ b/hr_attendance_work_entry/data/hr_attendance_work_entry_data.xml @@ -0,0 +1,17 @@ + + + + + Attendance + ATTN + + checked_in + fa-sign-in + + + + + Work Calendar + + + \ No newline at end of file diff --git a/hr_attendance_work_entry/models/__init__.py b/hr_attendance_work_entry/models/__init__.py new file mode 100644 index 00000000..1e37c49d --- /dev/null +++ b/hr_attendance_work_entry/models/__init__.py @@ -0,0 +1,3 @@ +from . import attendance +from . import employee +from . import work_entry diff --git a/hr_attendance_work_entry/models/attendance.py b/hr_attendance_work_entry/models/attendance.py new file mode 100644 index 00000000..273b1367 --- /dev/null +++ b/hr_attendance_work_entry/models/attendance.py @@ -0,0 +1,9 @@ +from odoo import fields, models + + +class HrAttendance(models.Model): + _inherit = 'hr.attendance' + + work_type_id = fields.Many2one('hr.work.entry.type', string='Work Type', + default=lambda self: self.env.ref('hr_attendance_work_entry.work_input_attendance', + raise_if_not_found=False)) diff --git a/hr_attendance_work_entry/models/employee.py b/hr_attendance_work_entry/models/employee.py new file mode 100644 index 00000000..d9caffc4 --- /dev/null +++ b/hr_attendance_work_entry/models/employee.py @@ -0,0 +1,58 @@ +from odoo import api, fields, models, _ +from odoo.exceptions import UserError + + +class HrEmployee(models.Model): + _inherit = 'hr.employee' + + attendance_state = fields.Selection(selection_add=[('break', 'Break')]) + + @api.depends('last_attendance_id.work_type_id') + def _compute_attendance_state(self): + for employee in self: + att = employee.last_attendance_id.sudo() + if not att or att.check_out: + employee.attendance_state = 'checked_out' + elif employee.last_attendance_id.work_type_id.attendance_state: + employee.attendance_state = employee.last_attendance_id.work_type_id.attendance_state + else: + employee.attendance_state = 'checked_in' + + def attendance_manual(self, next_action, entered_pin=None, work_type_id=None): + self = self.with_context(work_type_id=work_type_id) + return super(HrEmployee, self).attendance_manual(next_action, entered_pin=entered_pin) + + def _attendance_action_change(self): + """ Check In/Check Out action + Check In: create a new attendance record + Check Out: modify check_out field of appropriate attendance record + """ + self.ensure_one() + action_date = fields.Datetime.now() + work_type_id = self._context.get('work_type_id', False) + + if self.attendance_state == 'checked_out': + vals = { + 'employee_id': self.id, + 'check_in': action_date, + } + if work_type_id: + # if we don't have a work_type_id, we want the default + vals['work_type_id'] = work_type_id + return self.env['hr.attendance'].create(vals) + attendance = self.env['hr.attendance'].search([('employee_id', '=', self.id), ('check_out', '=', False)], limit=1) + if attendance and work_type_id: + # work_type_id is the "next" attendance type + attendance.check_out = action_date + vals = { + 'employee_id': self.id, + 'check_in': action_date, + 'work_type_id': work_type_id, + } + return self.env['hr.attendance'].create(vals) + if attendance: + attendance.check_out = action_date + else: + raise UserError(_('Cannot perform check out on %(empl_name)s, could not find corresponding check in. ' + 'Your attendances have probably been modified manually by human resources.') % {'empl_name': self.sudo().name, }) + return attendance diff --git a/hr_attendance_work_entry/models/work_entry.py b/hr_attendance_work_entry/models/work_entry.py new file mode 100644 index 00000000..b4088b0e --- /dev/null +++ b/hr_attendance_work_entry/models/work_entry.py @@ -0,0 +1,13 @@ +from odoo import fields, models + + +class HrWorkEntryType(models.Model): + _inherit = 'hr.work.entry.type' + + allow_attendance = fields.Boolean(string='Allow in Attendance') + attendance_icon = fields.Char(string='Attendance Icon', default='fa-sign-in') + attendance_state = fields.Selection([ + # ('checked_out', "Checked out"), # reserved for detecting new punch in + ('checked_in', "Checked in"), + ('break', 'Break'), + ], string='Attendance State', default='checked_in') diff --git a/hr_attendance_work_entry/tests/__init__.py b/hr_attendance_work_entry/tests/__init__.py new file mode 100644 index 00000000..170aaad8 --- /dev/null +++ b/hr_attendance_work_entry/tests/__init__.py @@ -0,0 +1 @@ +from . import test_attendance_work_type diff --git a/hr_attendance_work_entry/tests/test_attendance_work_type.py b/hr_attendance_work_entry/tests/test_attendance_work_type.py new file mode 100644 index 00000000..3b739d6d --- /dev/null +++ b/hr_attendance_work_entry/tests/test_attendance_work_type.py @@ -0,0 +1,47 @@ +from odoo.tests import common + + +class TestAttendanceWorkType(common.TransactionCase): + def setUp(self): + super().setUp() + self.employee = self.env.ref('hr.employee_hne') + self.default_work_type = self.env.ref('hr_attendance_work_entry.work_input_attendance') + + def test_01_work_type(self): + attendance = self.env['hr.attendance'].create({ + 'employee_id': self.employee.id, + 'check_in': '2020-01-06 10:00:00', # Monday + 'check_out': '2020-01-06 19:00:00', + }) + self.assertTrue(attendance.work_type_id) + self.assertEqual(attendance.work_type_id, self.default_work_type) + + def test_11_employee_clock_in(self): + self.assertEqual(self.employee.attendance_state, 'checked_out') + attendance = self.employee._attendance_action_change() + self.assertEqual(attendance.work_type_id, self.default_work_type) + self.assertEqual(self.employee.attendance_state, 'checked_in') + + # check out + self.employee._attendance_action_change() + self.assertEqual(self.employee.attendance_state, 'checked_out') + + def test_12_employee_clock_in_break(self): + # check in with non-standard work type + break_type = self.env['hr.work.entry.type'].create({ + 'name': 'Test Break', + 'code': 'TESTBREAK', + 'allow_attendance': True, + 'attendance_state': 'break', + }) + self.employee = self.employee.with_context(work_type_id=break_type.id) + attendance = self.employee._attendance_action_change() + self.assertEqual(attendance.work_type_id, break_type) + self.assertEqual(self.employee.attendance_state, 'break') + + # check back in immediately with default + self.employee = self.employee.with_context(work_type_id=self.default_work_type.id) + attendance = self.employee._attendance_action_change() + self.assertEqual(attendance.work_type_id, self.default_work_type) + self.assertEqual(attendance.work_type_id.attendance_state, 'checked_in') + self.assertEqual(self.employee.attendance_state, 'checked_in')