diff --git a/README.md b/README.md index 89789f74..72f67c7d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -[![Pre-commit Status](https://github.com/trevi-software/trevi-hr/actions/workflows/pre-commit.yml/badge.svg?branch=15.0)](https://github.com/trevi-software/trevi-hr/actions/workflows/pre-commit.yml?query=branch%3A15.0) -[![Build Status](https://github.com/trevi-software/trevi-hr/actions/workflows/test.yml/badge.svg?branch=15.0)](https://github.com/trevi-software/trevi-hr/actions/workflows/test.yml?query=branch%3A15.0) -[![codecov](https://codecov.io/gh/trevi-software/trevi-hr/branch/15.0/graph/badge.svg)](https://codecov.io/gh/trevi-software/trevi-hr) +[![Pre-commit Status](https://github.com/trevi-software/trevi-hr/actions/workflows/pre-commit.yml/badge.svg?branch=14.0)](https://github.com/trevi-software/trevi-hr/actions/workflows/pre-commit.yml?query=branch%3A14.0) +[![Build Status](https://github.com/trevi-software/trevi-hr/actions/workflows/test.yml/badge.svg?branch=14.0)](https://github.com/trevi-software/trevi-hr/actions/workflows/test.yml?query=branch%3A14.0) +[![codecov](https://codecov.io/gh/trevi-software/trevi-hr/branch/14.0/graph/badge.svg)](https://codecov.io/gh/trevi-software/trevi-hr) @@ -17,7 +17,63 @@ This repository contains Human Resource addons developed by TREVI Software [//]: # (addons) -This part will be replaced when running the oca-gen-addons-table script from OCA/maintainer-tools. +Available addons +---------------- +addon | version | maintainers | summary +--- | --- | --- | --- +[base_lock](base_lock/) | 14.0.1.0.0 | | Base locking module. +[group_payroll_manager](group_payroll_manager/) | 14.0.1.0.0 | | Permissions group Payroll Manager +[hr_accrual_bank](hr_accrual_bank/) | 14.0.1.0.0 | | Basic framework for recording accruals to a time bank +[hr_attendance_day](hr_attendance_day/) | 14.0.1.0.0 | | Attach a localized date to an attendace record +[hr_benefit](hr_benefit/) | 14.0.1.0.1 | | Assign benefits and deductables to employees +[hr_benefit_payroll](hr_benefit_payroll/) | 14.0.1.2.2 | | Access benefits in payroll through salary rules. +[hr_contract_status](hr_contract_status/) | 14.0.1.0.1 | | Workflows and notifications on employee contracts. +[hr_contract_status_benefit](hr_contract_status_benefit/) | 14.0.1.0.0 | | Link hr_contract_status with hr_benefit +[hr_contract_values](hr_contract_values/) | 14.0.1.0.0 | | Contracts - Initial Settings +[hr_contract_values_payroll](hr_contract_values_payroll/) | 14.0.1.1.0 | | Contract Payroll Structure Initial Settings +[hr_contract_values_resource_schedule](hr_contract_values_resource_schedule/) | 14.0.1.0.0 | | Set working hours in default contract values. +[hr_employee_seniority_months](hr_employee_seniority_months/) | 14.0.1.0.1 | | Calculate an employee's months of employment +[hr_employee_status](hr_employee_status/) | 14.0.1.0.2 | | Track the HR status of employees +[hr_employee_status_benefit](hr_employee_status_benefit/) | 14.0.1.0.0 | | Link between hr_employee_status and hr_benefit +[hr_employee_status_payroll](hr_employee_status_payroll/) | 14.0.1.0.0 | | Adds access records to employee separation records +[hr_employee_wizard](hr_employee_wizard/) | 14.0.1.0.0 | | Streamline the creation of a new employee record +[hr_job_change_state](hr_job_change_state/) | 14.0.1.0.0 | | Change State of Jobs +[hr_job_transfer](hr_job_transfer/) | 14.0.1.0.0 | | Departmental Transfer +[hr_jobs_hierarchy](hr_jobs_hierarchy/) | 14.0.1.0.0 | | Job Hierarchy +[hr_leave_type_unique](hr_leave_type_unique/) | 14.0.1.0.0 | | Ensure leave types are unique +[hr_photobooth](hr_photobooth/) | 14.0.1.0.0 | | Capture employee picture with webcam +[ir_module_category_payroll](ir_module_category_payroll/) | 14.0.1.0.0 | | Creates Payroll module category +[payroll_default_salary_rules](payroll_default_salary_rules/) | 14.0.1.0.0 | | Default set of salary rules and categories. +[payroll_operating_unit](payroll_operating_unit/) | 14.0.1.1.0 | | WARNING-this module will be removed. +[payroll_operating_unit_access_all](payroll_operating_unit_access_all/) | 14.0.1.1.0 | | Access all payslips. +[payroll_payslip_amendment](payroll_payslip_amendment/) | 14.0.1.0.0 | | Add amendments to current and future pay slips +[payroll_payslip_amendment_contract_status](payroll_payslip_amendment_contract_status/) | 14.0.1.0.0 | | Link payslip amendments with the employee contract state. +[payroll_payslip_dictionary](payroll_payslip_dictionary/) | 14.0.1.3.0 | | Dictionary of values that can be used in payslip calculations +[payroll_payslip_hr_leave_type](payroll_payslip_hr_leave_type/) | 14.0.1.0.0 | | Use time-off codes (instead of names) in payslip rules +[payroll_payslip_patch](payroll_payslip_patch/) | 14.0.2.0.0 | | Miscellaneous source code patches to payslip handling +[payroll_payslip_report](payroll_payslip_report/) | 14.0.2.0.0 | | Comprehensive payslip report by department. +[payroll_period_account](payroll_period_account/) | 14.0.1.0.0 | | Links payroll periods to accounting +[payroll_period_base_lock](payroll_period_base_lock/) | 14.0.1.0.0 | | Adds a base lock field to a payroll period. +[payroll_period_by_contract_type](payroll_period_by_contract_type/) | 14.0.1.0.0 | | Generate separate payslip batches for each contract type. +[payroll_period_contract_values](payroll_period_contract_values/) | 14.0.1.0.0 | | Links payroll period schedules with employee contracts. +[payroll_period_payslip_amendment](payroll_period_payslip_amendment/) | 14.0.1.0.0 | | Link payslip amendments with a payroll period. +[payroll_period_per_ou](payroll_period_per_ou/) | 14.0.1.0.0 | | Generate separate payroll sheets for each OU. +[payroll_period_processing](payroll_period_processing/) | 14.0.1.2.1 | | Payroll period processing wizard +[payroll_period_processing_per_ou](payroll_period_processing_per_ou/) | 14.0.1.0.0 | | For each period process only those payslips that belong to the OU. +[payroll_periods](payroll_periods/) | 14.0.1.4.0 | | Configurable payroll schedules. +[payroll_policy_absence](payroll_policy_absence/) | 14.0.1.0.0 | | Define properties of an employee absense policy for payroll. +[payroll_policy_accrual](payroll_policy_accrual/) | 14.0.1.0.0 | | Automatically or manually accrue to time banks to be withdrawn later +[payroll_policy_group](payroll_policy_group/) | 14.0.1.0.1 | | Group payroll policies and assign them to contracts +[payroll_policy_ot](payroll_policy_ot/) | 14.0.1.0.0 | | Assign over-time policies to a policy group +[payroll_policy_payslip](payroll_policy_payslip/) | 14.0.1.0.1 | | Apply payroll policies duing payslip processing +[payroll_policy_presence](payroll_policy_presence/) | 14.0.1.0.0 | | Define properties of an employee presence policy +[payroll_policy_rounding](payroll_policy_rounding/) | 14.0.1.0.0 | | Define attendance check-in and check-out rounding policies +[payroll_register](payroll_register/) | 14.0.1.3.0 | | Payroll Register +[payroll_register_report](payroll_register_report/) | 14.0.1.0.0 | | List payslips with salary categories by batch. +[res_currency_denomination](res_currency_denomination/) | 14.0.1.1.0 | | Currency Denominations +[resource_schedule](resource_schedule/) | 14.0.1.0.0 | | Easily create, manage, and track employee shift planning. +[trevi_hr_job_categories](trevi_hr_job_categories/) | 14.0.1.0.0 | | Job Categories +[trevi_hr_usability](trevi_hr_usability/) | 14.0.1.0.0 | | Simplify Employee Records. [//]: # (end addons) diff --git a/base_lock/README.rst b/base_lock/README.rst new file mode 100644 index 00000000..ba61cefd --- /dev/null +++ b/base_lock/README.rst @@ -0,0 +1,61 @@ +========= +Base Lock +========= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:a631edfee4e90a3340d15684c641555f8192c293e93e7af706d2ca7e26b2ee9a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-trevi--software%2Ftrevi--hr-lightgray.png?logo=github + :target: https://github.com/trevi-software/trevi-hr/tree/14.0/base_lock + :alt: trevi-software/trevi-hr + +|badge1| |badge2| |badge3| + +This module provides a base object locking mechanism. It offers no user visible functionality on its own. It is expected to be used by module developers. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* TREVI Software +* Michael Telahun Makonnen + +Other credits +~~~~~~~~~~~~~ + +* Michael Telahun Makonnen + +Maintainers +~~~~~~~~~~~ + +This module is part of the `trevi-software/trevi-hr `_ project on GitHub. + +You are welcome to contribute. diff --git a/base_lock/__init__.py b/base_lock/__init__.py new file mode 100644 index 00000000..2b0f2dca --- /dev/null +++ b/base_lock/__init__.py @@ -0,0 +1,5 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import models diff --git a/base_lock/__manifest__.py b/base_lock/__manifest__.py new file mode 100644 index 00000000..b7c94d36 --- /dev/null +++ b/base_lock/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + "name": "Base Lock", + "summary": "Base locking module.", + "version": "15.0.1.0.0", + "category": "Generic", + "images": ["static/src/img/main_screenshot.png"], + "author": "TREVI Software, Michael Telahun Makonnen", + "license": "AGPL-3", + "website": "https://github.com/trevi-software/trevi-hr", + "depends": ["base"], + "data": [ + "security/ir.model.access.csv", + ], + "installable": True, +} diff --git a/base_lock/i18n/base_lock.pot b/base_lock/i18n/base_lock.pot new file mode 100644 index 00000000..cd9db7f5 --- /dev/null +++ b/base_lock/i18n/base_lock.pot @@ -0,0 +1,79 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_lock +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: base_lock +#: model:ir.model,name:base_lock.model_base_lock +msgid "Base Lock Object" +msgstr "" + +#. module: base_lock +#: model:ir.model.fields,field_description:base_lock.field_base_lock__company_id +msgid "Company" +msgstr "" + +#. module: base_lock +#: model:ir.model.fields,field_description:base_lock.field_base_lock__create_uid +msgid "Created by" +msgstr "" + +#. module: base_lock +#: model:ir.model.fields,field_description:base_lock.field_base_lock__create_date +msgid "Created on" +msgstr "" + +#. module: base_lock +#: model:ir.model.fields,field_description:base_lock.field_base_lock__display_name +msgid "Display Name" +msgstr "" + +#. module: base_lock +#: model:ir.model.fields,field_description:base_lock.field_base_lock__end_time +msgid "End Time" +msgstr "" + +#. module: base_lock +#: model:ir.model.fields,field_description:base_lock.field_base_lock__id +msgid "ID" +msgstr "" + +#. module: base_lock +#: model:ir.model.fields,field_description:base_lock.field_base_lock____last_update +msgid "Last Modified on" +msgstr "" + +#. module: base_lock +#: model:ir.model.fields,field_description:base_lock.field_base_lock__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: base_lock +#: model:ir.model.fields,field_description:base_lock.field_base_lock__write_date +msgid "Last Updated on" +msgstr "" + +#. module: base_lock +#: model:ir.model.fields,field_description:base_lock.field_base_lock__name +msgid "Name" +msgstr "" + +#. module: base_lock +#: model:ir.model.fields,field_description:base_lock.field_base_lock__start_time +msgid "Start Time" +msgstr "" + +#. module: base_lock +#: model:ir.model.fields,field_description:base_lock.field_base_lock__tz +msgid "Time Zone" +msgstr "" diff --git a/base_lock/models/__init__.py b/base_lock/models/__init__.py new file mode 100644 index 00000000..ab127a31 --- /dev/null +++ b/base_lock/models/__init__.py @@ -0,0 +1,5 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import base_lock diff --git a/base_lock/models/base_lock.py b/base_lock/models/base_lock.py new file mode 100644 index 00000000..0ecc5004 --- /dev/null +++ b/base_lock/models/base_lock.py @@ -0,0 +1,80 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from datetime import datetime + +from pytz import common_timezones, timezone, utc + +from odoo import api, fields, models +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT as OE_DTFORMAT + + +class Lock(models.Model): + + _name = "base.lock" + _description = "Base Lock Object" + _check_company_auto = True + + @api.model + def _tz_list(self): + + res = tuple() + for name in common_timezones: + res += ((name, name),) + return res + + name = fields.Char(required=True) + start_time = fields.Datetime(required=True) + end_time = fields.Datetime(required=True) + tz = fields.Selection(selection=_tz_list, string="Time Zone", required=True) + company_id = fields.Many2one( + comodel_name="res.company", + string="Company", + index=True, + default=lambda self: self.env.company, + required=True, + ) + + @api.model + def create(self, vals): + dt_tz = timezone(vals.get("tz", False)) + dtStart = vals.get("start_time", False) + dtEnd = vals.get("end_time", False) + tzStart = dt_tz.localize(dtStart, is_dst=False) + tzEnd = dt_tz.localize(dtEnd, is_dst=False) + dtStart = tzStart.astimezone(utc).replace(tzinfo=None) + dtEnd = tzEnd.astimezone(utc).replace(tzinfo=None) + vals["start_time"] = dtStart + vals["end_time"] = dtEnd + + return super(Lock, self).create(vals) + + @api.model + def is_locked_datetime_utc(self, dt_str): + """Determines whether a DateTime (string) value falls within a locked period. + The DateTime string is assumed to be a naive UTC (straight from DB).""" + + lock_ids = self.search( + ["&", ("start_time", "<=", dt_str), ("end_time", ">=", dt_str)] + ) + if len(lock_ids) > 0: + return True + + return False + + @api.model + def is_locked_date(self, d_str, tz_str=None): + """Determine if the date (string) is locked. If a time zone is + specified it will check for midnight according to it, otherwise, + it is assumed to be UTC""" + + dt_str = d_str + " 00:00:00" + if tz_str: + dt_tz = timezone(tz_str) + dt = datetime.strptime(dt_str, OE_DTFORMAT) + tzdt = dt_tz.localize(dt, is_dst=False) + utcdt = tzdt.astimezone(utc) + dt_str = utcdt.strftime(OE_DTFORMAT) + + return self.is_locked_datetime_utc(dt_str) diff --git a/base_lock/readme/CREDITS.rst b/base_lock/readme/CREDITS.rst new file mode 100644 index 00000000..d264bc7e --- /dev/null +++ b/base_lock/readme/CREDITS.rst @@ -0,0 +1 @@ +* Michael Telahun Makonnen diff --git a/base_lock/readme/DESCRIPTION.rst b/base_lock/readme/DESCRIPTION.rst new file mode 100644 index 00000000..3a9ffc1b --- /dev/null +++ b/base_lock/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module provides a base object locking mechanism. It offers no user visible functionality on its own. It is expected to be used by module developers. diff --git a/base_lock/security/ir.model.access.csv b/base_lock/security/ir.model.access.csv new file mode 100644 index 00000000..d52508d5 --- /dev/null +++ b/base_lock/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_base_lock_user,access_base_lock,model_base_lock,base.group_user,1,0,0,0 diff --git a/base_lock/static/description/icon.png b/base_lock/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/base_lock/static/description/icon.png differ diff --git a/base_lock/static/description/index.html b/base_lock/static/description/index.html new file mode 100644 index 00000000..83c56e7a --- /dev/null +++ b/base_lock/static/description/index.html @@ -0,0 +1,417 @@ + + + + + + +Base Lock + + + +
+

Base Lock

+ + +

Beta License: AGPL-3 trevi-software/trevi-hr

+

This module provides a base object locking mechanism. It offers no user visible functionality on its own. It is expected to be used by module developers.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • TREVI Software
  • +
  • Michael Telahun Makonnen
  • +
+
+
+

Other credits

+ +
+
+

Maintainers

+

This module is part of the trevi-software/trevi-hr project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/base_lock/static/src/img/main_screenshot.png b/base_lock/static/src/img/main_screenshot.png new file mode 100644 index 00000000..ab78e067 Binary files /dev/null and b/base_lock/static/src/img/main_screenshot.png differ diff --git a/base_lock/tests/__init__.py b/base_lock/tests/__init__.py new file mode 100644 index 00000000..79e5a3f6 --- /dev/null +++ b/base_lock/tests/__init__.py @@ -0,0 +1,5 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import test_lock diff --git a/base_lock/tests/test_lock.py b/base_lock/tests/test_lock.py new file mode 100644 index 00000000..ec800ac6 --- /dev/null +++ b/base_lock/tests/test_lock.py @@ -0,0 +1,137 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from datetime import datetime + +from odoo.exceptions import AccessError +from odoo.tests import common, new_test_user + + +class TestLock(common.SavepointCase): + @classmethod + def setUpClass(cls): + super(TestLock, cls).setUpClass() + + cls.Lock = cls.env["base.lock"] + + # normal user + cls.user = new_test_user( + cls.env, + login="reg", + groups="base.group_user", + name="Simple employee", + email="reg@example.com", + ) + + def create_lock(self, start, end, tz): + return self.Lock.create( + { + "name": "A lock", + "start_time": start, + "end_time": end, + "tz": tz, + } + ) + + def test_user_read(self): + """Has read access to lock.lock""" + + # utc: 2020-12-31 21:00:00 - 2021-01-31 20:59:59 + start = datetime(2021, 1, 1, 0, 0, 0) + end = datetime(2021, 1, 31, 23, 59, 59) + lk = self.create_lock(start, end, "Africa/Addis_Ababa") + try: + lk.with_user(self.user.id).read([]) + except AccessError: + self.fail("raised an AccessError exception") + + def test_user_write_fails(self): + """Write access fails""" + + # utc: 2020-12-31 21:00:00 - 2021-01-31 20:59:59 + start = datetime(2021, 1, 1, 0, 0, 0) + end = datetime(2021, 1, 31, 23, 59, 59) + lk = self.create_lock(start, end, "Africa/Addis_Ababa") + with self.assertRaises(AccessError): + lk.with_user(self.user.id).name = "B" + + def test_user_create_fails(self): + """Create access fails""" + + # utc: 2020-12-31 21:00:00 - 2021-01-31 20:59:59 + start = datetime(2021, 1, 1, 0, 0, 0) + end = datetime(2021, 1, 31, 23, 59, 59) + with self.assertRaises(AccessError): + self.Lock.with_user(self.user.id).create( + { + "name": "A", + "start_time": start, + "end_time": end, + "tz": "Africa/Addis_Ababa", + } + ) + + def test_user_unlink_fails(self): + """Unlink access fails""" + + # utc: 2020-12-31 21:00:00 - 2021-01-31 20:59:59 + start = datetime(2021, 1, 1, 0, 0, 0) + end = datetime(2021, 1, 31, 23, 59, 59) + lk = self.create_lock(start, end, "Africa/Addis_Ababa") + with self.assertRaises(AccessError): + lk.with_user(self.user.id).unlink() + + def test_lock_datetime_utc(self): + """Locking of datetime value""" + + # utc: 2020-12-31 21:00:00 - 2021-01-31 20:59:59 + start = datetime(2021, 1, 1, 0, 0, 0) + end = datetime(2021, 1, 31, 23, 59, 59) + + lk = self.create_lock(start, end, "Africa/Addis_Ababa") + + # just before lock start + self.assertFalse(lk.is_locked_datetime_utc("2020-12-31 02:59:59")) + # just after lock end + self.assertFalse(lk.is_locked_datetime_utc("2021-01-31 21:00:00")) + # at lock start + self.assertTrue(lk.is_locked_datetime_utc("2020-12-31 21:00:00")) + # in the middle of the lock period + self.assertTrue(lk.is_locked_datetime_utc("2021-01-15 03:00:00")) + # at lock end + self.assertTrue(lk.is_locked_datetime_utc("2021-01-31 20:59:59")) + + def test_lock_date_tz(self): + """Locking of date value with timezone info""" + + # utc: 2020-12-31 21:00:00 - 2021-01-31 20:59:59 + start = datetime(2021, 1, 1, 0, 0, 0) + end = datetime(2021, 1, 31, 23, 59, 59) + lk = self.create_lock(start, end, "Africa/Addis_Ababa") + + # day before lock start + self.assertFalse(lk.is_locked_date("2020-12-31", "Africa/Addis_Ababa")) + # day after lock end + self.assertFalse(lk.is_locked_date("2021-02-01", "Africa/Addis_Ababa")) + # day of lock start + self.assertTrue(lk.is_locked_date("2021-01-01", "Africa/Addis_Ababa")) + # day of lock end + self.assertTrue(lk.is_locked_date("2021-01-31", "Africa/Addis_Ababa")) + + def test_lock_date_notz(self): + """Locking of date value without timezone info""" + + # utc: 2021-01-01 00:00:00 - 2021-01-31 23:59:59 + start = datetime(2021, 1, 1, 0, 0, 0) + end = datetime(2021, 1, 31, 23, 59, 59) + lk = self.create_lock(start, end, "Africa/Addis_Ababa") + + # day before lock start + self.assertFalse(lk.is_locked_date("2020-12-31")) + # day after lock end + self.assertFalse(lk.is_locked_date("2021-02-01")) + # day of lock start + self.assertTrue(lk.is_locked_date("2021-01-01")) + # day of lock end + self.assertTrue(lk.is_locked_date("2021-01-31")) diff --git a/hr_accrual_bank/README.rst b/hr_accrual_bank/README.rst new file mode 100644 index 00000000..d1a1aba4 --- /dev/null +++ b/hr_accrual_bank/README.rst @@ -0,0 +1,72 @@ +========= +Time Bank +========= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:de6edbdb5fdc15c355783318d1bb1cc52bd0b0368b433f8ffe5940a234bc9b3f + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-trevi--software%2Ftrevi--hr-lightgray.png?logo=github + :target: https://github.com/trevi-software/trevi-hr/tree/14.0/hr_accrual_bank + :alt: trevi-software/trevi-hr + +|badge1| |badge2| |badge3| + +Accruals to Bank +================ + +An Accrual is any benefit (usually time) that accrues on behalf of an employee over an extended +period of time. This can be vacation days, sick days, or some other benefit. The actual policy +and mechanics of bank accrual should be handled by other modules. This module only provides +the basic framework for recording the data. While this module's functionality overlaps with +Odoo's accrual leave allocations they are not the same and are not interchangeable. Specifically, this +module: +* Allows more complex accrual accounting (for example 1.5 days for every month for the first 12 months, then 1.75 days for the next 12 months, etc...) +* Allows creation of accruals from salary rules during payroll processing +* Does not attach units to the accrued amount (1.0 can mean a vacation day, a dollar, a product, or anything else depending on the module) + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* TREVI Software +* Michael Telahun Makonnen + +Other credits +~~~~~~~~~~~~~ + +* Michael Telahun Makonnen + +Maintainers +~~~~~~~~~~~ + +This module is part of the `trevi-software/trevi-hr `_ project on GitHub. + +You are welcome to contribute. diff --git a/hr_accrual_bank/__init__.py b/hr_accrual_bank/__init__.py new file mode 100644 index 00000000..1cc5116e --- /dev/null +++ b/hr_accrual_bank/__init__.py @@ -0,0 +1,5 @@ +# Copyright (C) 2021 TREVI Software +# Copyright (C) 2013 Michael Telahun Makonnen . +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models diff --git a/hr_accrual_bank/__manifest__.py b/hr_accrual_bank/__manifest__.py new file mode 100644 index 00000000..ec15a935 --- /dev/null +++ b/hr_accrual_bank/__manifest__.py @@ -0,0 +1,24 @@ +# Copyright (C) 2021 TREVI Software +# Copyright (C) 2013 Michael Telahun Makonnen . +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Time Bank", + "summary": "Basic framework for recording accruals to a time bank", + "version": "15.0.1.0.0", + "category": "Human Resources", + "author": "TREVI Software, Michael Telahun Makonnen", + "license": "AGPL-3", + "images": ["static/src/img/main_screenshot.png"], + "website": "https://github.com/trevi-software/trevi-hr", + "depends": [ + "hr", + "hr_holidays", + ], + "data": [ + "security/ir.model.access.csv", + "data/accrual_data.xml", + "views/hr_accrual_view.xml", + ], + "installable": True, +} diff --git a/hr_accrual_bank/data/accrual_data.xml b/hr_accrual_bank/data/accrual_data.xml new file mode 100644 index 00000000..ecb14bac --- /dev/null +++ b/hr_accrual_bank/data/accrual_data.xml @@ -0,0 +1,12 @@ + + + + + + + Accruals + 3 + + + + diff --git a/hr_accrual_bank/i18n/hr_accrual_bank.pot b/hr_accrual_bank/i18n/hr_accrual_bank.pot new file mode 100644 index 00000000..e6b4be4d --- /dev/null +++ b/hr_accrual_bank/i18n/hr_accrual_bank.pot @@ -0,0 +1,129 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_accrual_bank +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: hr_accrual_bank +#: model:ir.model,name:hr_accrual_bank.model_hr_accrual +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_accrual_line__accrual_id +#: model_terms:ir.ui.view,arch_db:hr_accrual_bank.hr_accrual_view_form +msgid "Accrual" +msgstr "" + +#. module: hr_accrual_bank +#: model:ir.model,name:hr_accrual_bank.model_hr_accrual_line +msgid "Accrual Line" +msgstr "" + +#. module: hr_accrual_bank +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_accrual__line_ids +#: model_terms:ir.ui.view,arch_db:hr_accrual_bank.hr_accrual_view_form +msgid "Accrual Lines" +msgstr "" + +#. module: hr_accrual_bank +#: model:ir.actions.act_window,name:hr_accrual_bank.open_accrual +#: model:ir.ui.menu,name:hr_accrual_bank.menu_hr_accrual +msgid "Accrual Time Banks" +msgstr "" + +#. module: hr_accrual_bank +#: model_terms:ir.ui.view,arch_db:hr_accrual_bank.hr_accrual_view_tree +msgid "Accruals" +msgstr "" + +#. module: hr_accrual_bank +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_accrual_line__amount +msgid "Amount" +msgstr "" + +#. module: hr_accrual_bank +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_accrual__create_uid +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_accrual_line__create_uid +msgid "Created by" +msgstr "" + +#. module: hr_accrual_bank +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_accrual__create_date +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_accrual_line__create_date +msgid "Created on" +msgstr "" + +#. module: hr_accrual_bank +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_accrual_line__date +msgid "Date" +msgstr "" + +#. module: hr_accrual_bank +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_accrual__display_name +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_accrual_line__display_name +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_leave_allocation__display_name +msgid "Display Name" +msgstr "" + +#. module: hr_accrual_bank +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_accrual_line__employee_id +msgid "Employee" +msgstr "" + +#. module: hr_accrual_bank +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_leave_allocation__from_accrual +msgid "From Accrual" +msgstr "" + +#. module: hr_accrual_bank +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_accrual__id +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_accrual_line__id +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_leave_allocation__id +msgid "ID" +msgstr "" + +#. module: hr_accrual_bank +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_accrual____last_update +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_accrual_line____last_update +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_leave_allocation____last_update +msgid "Last Modified on" +msgstr "" + +#. module: hr_accrual_bank +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_accrual__write_uid +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_accrual_line__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: hr_accrual_bank +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_accrual__write_date +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_accrual_line__write_date +msgid "Last Updated on" +msgstr "" + +#. module: hr_accrual_bank +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_accrual__holiday_status_id +msgid "Leave" +msgstr "" + +#. module: hr_accrual_bank +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_accrual_line__leave_allocation_id +msgid "Leave Allocation" +msgstr "" + +#. module: hr_accrual_bank +#: model:ir.model.fields,field_description:hr_accrual_bank.field_hr_accrual__name +#: model_terms:ir.ui.view,arch_db:hr_accrual_bank.hr_accrual_view_form +msgid "Name" +msgstr "" + +#. module: hr_accrual_bank +#: model:ir.model,name:hr_accrual_bank.model_hr_leave_allocation +msgid "Time Off Allocation" +msgstr "" diff --git a/hr_accrual_bank/models/__init__.py b/hr_accrual_bank/models/__init__.py new file mode 100644 index 00000000..004aefc7 --- /dev/null +++ b/hr_accrual_bank/models/__init__.py @@ -0,0 +1,6 @@ +# Copyright (C) 2021 TREVI Software +# Copyright (C) 2013 Michael Telahun Makonnen . +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import hr_accrual +from . import hr_leave_allocation diff --git a/hr_accrual_bank/models/hr_accrual.py b/hr_accrual_bank/models/hr_accrual.py new file mode 100644 index 00000000..ca3e301d --- /dev/null +++ b/hr_accrual_bank/models/hr_accrual.py @@ -0,0 +1,88 @@ +# Copyright (C) 2021 TREVI Software +# Copyright (C) 2013 Michael Telahun Makonnen . +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class HrAccrual(models.Model): + + _name = "hr.accrual" + _description = "Accrual" + + name = fields.Char(required=True) + holiday_status_id = fields.Many2one(string="Leave", comodel_name="hr.leave.type") + line_ids = fields.One2many( + string="Accrual Lines", + comodel_name="hr.accrual.line", + inverse_name="accrual_id", + readonly=True, + ) + + def get_balance(self, employee_id, date=None): + + if date is None: + date = fields.Date.today() + + res = 0.0 + self.env.cr.execute( + """SELECT SUM(amount) from hr_accrual_line \ + WHERE accrual_id in %s AND employee_id=%s AND date <= %s""", + (tuple(self.ids), employee_id, date), + ) + for row in self.env.cr.fetchall(): + res = row[0] + + return res + + def deposit(self, employee_id, amount, date, name=None): + + line_obj = self.env["hr.accrual.line"] + + res = [] + for accrual in self: + + lv = False + if accrual.holiday_status_id: + leave_allocation = { + "name": name is not None and name or "Allocation from Accrual", + "allocation_type": "regular", + "state": "draft", + "employee_id": employee_id, + "number_of_days": amount, + "holiday_status_id": accrual.holiday_status_id.id, + "from_accrual": True, + } + lv = self.env["hr.leave.allocation"].create(leave_allocation) + lv.action_confirm() + lv.action_validate() + + # Create accrual line + # + vals = { + "date": date, + "employee_id": employee_id, + "amount": amount, + "accrual_id": accrual.id, + "leave_allocation_id": lv and lv.id or False, + } + res.append(line_obj.create(vals)) + + return res + + +class HrAccrualLine(models.Model): + + _name = "hr.accrual.line" + _description = "Accrual Line" + _rec_name = "date" + + date = fields.Date(required=True, default=fields.Date.today()) + accrual_id = fields.Many2one( + string="Accrual", comodel_name="hr.accrual", required=True + ) + employee_id = fields.Many2one( + string="Employee", comodel_name="hr.employee", required=True + ) + leave_allocation_id = fields.Many2one("hr.leave.allocation") + amount = fields.Float(digits="Accruals", required=True) diff --git a/hr_accrual_bank/models/hr_leave_allocation.py b/hr_accrual_bank/models/hr_leave_allocation.py new file mode 100644 index 00000000..6fa86008 --- /dev/null +++ b/hr_accrual_bank/models/hr_leave_allocation.py @@ -0,0 +1,10 @@ +# Copyright (C) 2021 TREVI Software +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class HrLeaveAllocation(models.Model): + _inherit = "hr.leave.allocation" + + from_accrual = fields.Boolean() diff --git a/hr_accrual_bank/readme/CREDITS.rst b/hr_accrual_bank/readme/CREDITS.rst new file mode 100644 index 00000000..d264bc7e --- /dev/null +++ b/hr_accrual_bank/readme/CREDITS.rst @@ -0,0 +1 @@ +* Michael Telahun Makonnen diff --git a/hr_accrual_bank/readme/DESCRIPTION.rst b/hr_accrual_bank/readme/DESCRIPTION.rst new file mode 100644 index 00000000..52234028 --- /dev/null +++ b/hr_accrual_bank/readme/DESCRIPTION.rst @@ -0,0 +1,12 @@ +Accruals to Bank +================ + +An Accrual is any benefit (usually time) that accrues on behalf of an employee over an extended +period of time. This can be vacation days, sick days, or some other benefit. The actual policy +and mechanics of bank accrual should be handled by other modules. This module only provides +the basic framework for recording the data. While this module's functionality overlaps with +Odoo's accrual leave allocations they are not the same and are not interchangeable. Specifically, this +module: +* Allows more complex accrual accounting (for example 1.5 days for every month for the first 12 months, then 1.75 days for the next 12 months, etc...) +* Allows creation of accruals from salary rules during payroll processing +* Does not attach units to the accrued amount (1.0 can mean a vacation day, a dollar, a product, or anything else depending on the module) diff --git a/hr_accrual_bank/security/ir.model.access.csv b/hr_accrual_bank/security/ir.model.access.csv new file mode 100644 index 00000000..e1b1ecb4 --- /dev/null +++ b/hr_accrual_bank/security/ir.model.access.csv @@ -0,0 +1,7 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_hr_accrual_user,access_hr_accrual,model_hr_accrual,hr.group_hr_user,1,0,0,0 +access_hr_accrual_line_user,access_hr_accrual_line,model_hr_accrual_line,hr.group_hr_user,1,0,0,0 +access_hr_accrual_leave_mgr,access_hr_accrual,model_hr_accrual,hr_holidays.group_hr_holidays_manager,1,1,1,1 +access_hr_accrual_leave_resp,access_hr_accrual,model_hr_accrual,hr_holidays.group_hr_holidays_responsible,1,0,0,0 +access_hr_accrual_line_leave_mgr,access_hr_accrual_line,model_hr_accrual_line,hr_holidays.group_hr_holidays_manager,1,1,1,1 +access_hr_accrual_line_leave_resp,access_hr_accrual_line,model_hr_accrual_line,hr_holidays.group_hr_holidays_responsible,1,0,0,0 diff --git a/hr_accrual_bank/static/description/icon.png b/hr_accrual_bank/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/hr_accrual_bank/static/description/icon.png differ diff --git a/hr_accrual_bank/static/description/index.html b/hr_accrual_bank/static/description/index.html new file mode 100644 index 00000000..116f9e3d --- /dev/null +++ b/hr_accrual_bank/static/description/index.html @@ -0,0 +1,417 @@ + + + + + + +Time Bank + + + +
+

Time Bank

+ + +

Beta License: AGPL-3 trevi-software/trevi-hr

+
+

Accruals to Bank

+

An Accrual is any benefit (usually time) that accrues on behalf of an employee over an extended +period of time. This can be vacation days, sick days, or some other benefit. The actual policy +and mechanics of bank accrual should be handled by other modules. This module only provides +the basic framework for recording the data. While this module’s functionality overlaps with +Odoo’s accrual leave allocations they are not the same and are not interchangeable. Specifically, this +module: +* Allows more complex accrual accounting (for example 1.5 days for every month for the first 12 months, then 1.75 days for the next 12 months, etc…) +* Allows creation of accruals from salary rules during payroll processing +* Does not attach units to the accrued amount (1.0 can mean a vacation day, a dollar, a product, or anything else depending on the module)

+

Table of contents

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • TREVI Software
  • +
  • Michael Telahun Makonnen
  • +
+
+
+

Other credits

+ +
+
+

Maintainers

+

This module is part of the trevi-software/trevi-hr project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/hr_accrual_bank/static/src/img/main_screenshot.png b/hr_accrual_bank/static/src/img/main_screenshot.png new file mode 100644 index 00000000..ab78e067 Binary files /dev/null and b/hr_accrual_bank/static/src/img/main_screenshot.png differ diff --git a/hr_accrual_bank/tests/__init__.py b/hr_accrual_bank/tests/__init__.py new file mode 100644 index 00000000..4758ffd4 --- /dev/null +++ b/hr_accrual_bank/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2021 TREVI Software +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_hr_accrual diff --git a/hr_accrual_bank/tests/test_hr_accrual.py b/hr_accrual_bank/tests/test_hr_accrual.py new file mode 100644 index 00000000..8206ca57 --- /dev/null +++ b/hr_accrual_bank/tests/test_hr_accrual.py @@ -0,0 +1,56 @@ +# Copyright (C) 2021 TREVI Software +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields +from odoo.tests import common + + +class TestAccrual(common.SavepointCase): + @classmethod + def setUpClass(cls): + super(TestAccrual, cls).setUpClass() + + cls.Accrual = cls.env["hr.accrual"] + cls.Allocation = cls.env["hr.leave.allocation"] + cls.eeJohn = cls.env["hr.employee"].create({"name": "EE John"}) + + # Make sure we have the rights to create, validate and delete the + # leaves, leave types and allocations + LeaveType = cls.env["hr.leave.type"].with_context(tracking_disable=True) + + cls.accrual_type = LeaveType.create( + { + "name": "accrual", + "allocation_type": "fixed", + "validity_start": False, + } + ) + + def test_accrual_deposit_and_balance(self): + """The balance should be the same amount deposited""" + + accrual = self.Accrual.create({"name": "A"}) + accrual.deposit(self.eeJohn.id, 100, fields.Date.today(), "manual accrual") + self.assertEqual(100, accrual.get_balance(self.eeJohn.id)) + + def test_create_leave_allocation(self): + """A deposit should create a corresponding leave allocation""" + + accrual = self.Accrual.create( + {"name": "A", "holiday_status_id": self.accrual_type.id} + ) + accrual.deposit(self.eeJohn.id, 2.0, fields.Date.today(), "manual accrual") + + allocs = self.Allocation.search([("name", "=", "manual accrual")]) + self.assertEqual(1, len(allocs)) + self.assertTrue(allocs[0].from_accrual) + self.assertEqual(allocs[0].allocation_type, "regular") + self.assertEqual( + 0, + fields.Float.compare( + allocs[0].number_of_days_display, 2.0, precision_digits=1 + ), + ) + self.assertEqual( + 0, fields.Float.compare(allocs[0].number_of_days, 2.0, precision_digits=1) + ) diff --git a/hr_accrual_bank/views/hr_accrual_view.xml b/hr_accrual_bank/views/hr_accrual_view.xml new file mode 100644 index 00000000..bd7cc63a --- /dev/null +++ b/hr_accrual_bank/views/hr_accrual_view.xml @@ -0,0 +1,50 @@ + + + + + + hr.accrual.tree + hr.accrual + + + + + + + + + + hr.accrual.form + hr.accrual + +
+
+
+ + + Accrual Time Banks + hr.accrual + tree,form + + + +
+
diff --git a/hr_attendance_day/README.rst b/hr_attendance_day/README.rst new file mode 100644 index 00000000..7a99e3f9 --- /dev/null +++ b/hr_attendance_day/README.rst @@ -0,0 +1,61 @@ +============== +Attendance Day +============== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:b461f524a00eb5a7a4a48e76056776fbe1b24def17935bf20729c00b7b6662f8 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-trevi--software%2Ftrevi--hr-lightgray.png?logo=github + :target: https://github.com/trevi-software/trevi-hr/tree/14.0/hr_attendance_day + :alt: trevi-software/trevi-hr + +|badge1| |badge2| |badge3| + +This module attaches an attendance "date" field to each attendance record to signify which calendar date the record +belongs to. The date is calculated according to the employee's timezone. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* TREVI Software + +Other credits +~~~~~~~~~~~~~ + +Michael Telahun Makonnen + +Maintainers +~~~~~~~~~~~ + +This module is part of the `trevi-software/trevi-hr `_ project on GitHub. + +You are welcome to contribute. diff --git a/hr_attendance_day/__init__.py b/hr_attendance_day/__init__.py new file mode 100644 index 00000000..35bb139f --- /dev/null +++ b/hr_attendance_day/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2022 TREVI Software +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models diff --git a/hr_attendance_day/__manifest__.py b/hr_attendance_day/__manifest__.py new file mode 100644 index 00000000..9a79af58 --- /dev/null +++ b/hr_attendance_day/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright (C) 2022 TREVI Software +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Attendance Day", + "summary": "Attach a localized date to an attendace record", + "version": "15.0.1.0.0", + "category": "Human Resources", + "author": "TREVI Software", + "license": "AGPL-3", + "images": ["static/src/img/main_screenshot.png"], + "website": "https://github.com/trevi-software/trevi-hr", + "depends": [ + "hr", + "hr_attendance", + ], + "data": [ + "views/hr_attendance_view.xml", + ], + "installable": True, +} diff --git a/hr_attendance_day/i18n/hr_attendance_day.pot b/hr_attendance_day/i18n/hr_attendance_day.pot new file mode 100644 index 00000000..ce984806 --- /dev/null +++ b/hr_attendance_day/i18n/hr_attendance_day.pot @@ -0,0 +1,39 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_attendance_day +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: hr_attendance_day +#: model:ir.model,name:hr_attendance_day.model_hr_attendance +msgid "Attendance" +msgstr "" + +#. module: hr_attendance_day +#: model:ir.model.fields,field_description:hr_attendance_day.field_hr_attendance__day +msgid "Day" +msgstr "" + +#. module: hr_attendance_day +#: model:ir.model.fields,field_description:hr_attendance_day.field_hr_attendance__display_name +msgid "Display Name" +msgstr "" + +#. module: hr_attendance_day +#: model:ir.model.fields,field_description:hr_attendance_day.field_hr_attendance__id +msgid "ID" +msgstr "" + +#. module: hr_attendance_day +#: model:ir.model.fields,field_description:hr_attendance_day.field_hr_attendance____last_update +msgid "Last Modified on" +msgstr "" diff --git a/hr_attendance_day/models/__init__.py b/hr_attendance_day/models/__init__.py new file mode 100644 index 00000000..2064bbcb --- /dev/null +++ b/hr_attendance_day/models/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2022 TREVI Software +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import hr_attendance diff --git a/hr_attendance_day/models/hr_attendance.py b/hr_attendance_day/models/hr_attendance.py new file mode 100644 index 00000000..e100a9f3 --- /dev/null +++ b/hr_attendance_day/models/hr_attendance.py @@ -0,0 +1,21 @@ +# Copyright (C) 2022 Trevi Software (https://trevi.et) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from pytz import timezone, utc + +from odoo import api, fields, models + + +class HrAttendance(models.Model): + _inherit = "hr.attendance" + + day = fields.Date(compute="_compute_day", store=True) + + @api.depends("check_in") + def _compute_day(self): + + for att in self: + local_tz = timezone(att.employee_id.tz) + utc_check_in = utc.localize(att.check_in) + tz_check_in = utc_check_in.astimezone(local_tz) + att.day = tz_check_in.date() diff --git a/hr_attendance_day/readme/CREDITS.rst b/hr_attendance_day/readme/CREDITS.rst new file mode 100644 index 00000000..47150bbd --- /dev/null +++ b/hr_attendance_day/readme/CREDITS.rst @@ -0,0 +1 @@ +Michael Telahun Makonnen diff --git a/hr_attendance_day/readme/DESCRIPTION.rst b/hr_attendance_day/readme/DESCRIPTION.rst new file mode 100644 index 00000000..94965b28 --- /dev/null +++ b/hr_attendance_day/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module attaches an attendance "date" field to each attendance record to signify which calendar date the record +belongs to. The date is calculated according to the employee's timezone. diff --git a/hr_attendance_day/static/description/icon.png b/hr_attendance_day/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/hr_attendance_day/static/description/icon.png differ diff --git a/hr_attendance_day/static/description/index.html b/hr_attendance_day/static/description/index.html new file mode 100644 index 00000000..c0b2cccb --- /dev/null +++ b/hr_attendance_day/static/description/index.html @@ -0,0 +1,415 @@ + + + + + + +Attendance Day + + + +
+

Attendance Day

+ + +

Beta License: AGPL-3 trevi-software/trevi-hr

+

This module attaches an attendance “date” field to each attendance record to signify which calendar date the record +belongs to. The date is calculated according to the employee’s timezone.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • TREVI Software
  • +
+
+
+

Other credits

+

Michael Telahun Makonnen <mtm@trevi.et>

+
+
+

Maintainers

+

This module is part of the trevi-software/trevi-hr project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/hr_attendance_day/static/src/img/main_screenshot.png b/hr_attendance_day/static/src/img/main_screenshot.png new file mode 100644 index 00000000..ab78e067 Binary files /dev/null and b/hr_attendance_day/static/src/img/main_screenshot.png differ diff --git a/hr_attendance_day/tests/__init__.py b/hr_attendance_day/tests/__init__.py new file mode 100644 index 00000000..16bc4606 --- /dev/null +++ b/hr_attendance_day/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2022 TREVI Software +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_hr_attendance diff --git a/hr_attendance_day/tests/test_hr_attendance.py b/hr_attendance_day/tests/test_hr_attendance.py new file mode 100644 index 00000000..48549c2d --- /dev/null +++ b/hr_attendance_day/tests/test_hr_attendance.py @@ -0,0 +1,96 @@ +# Copyright (C) 2022 Trevi Software (https://trevi.et) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from datetime import date, datetime + +from odoo.tests import common + + +class TestHrAttendance(common.SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.Attendance = cls.env["hr.attendance"] + + # Africa/Accra is UTC + 0:00 + cls.john = cls.env["hr.employee"].create({"name": "John", "tz": "Africa/Accra"}) + + # Africa/Addis_Ababa is UTC + 3:00 + cls.sally = cls.env["hr.employee"].create( + {"name": "Sally", "tz": "Africa/Addis_Ababa"} + ) + + def test_utc_and_tz_match(self): + + check_in = datetime(2022, 4, 1, 23, 59, 59) + check_out = datetime(2022, 4, 2, 8, 0) + att = self.Attendance.create( + { + "employee_id": self.john.id, + "check_in": check_in, + "check_out": check_out, + } + ) + + self.assertEqual( + att.day, + date(2022, 4, 1), + "Timezone with zero offset (1) from UTC has correct date", + ) + + check_in = datetime(2022, 4, 3, 0, 0, 0) + check_out = datetime(2022, 4, 3, 8, 0) + att = self.Attendance.create( + { + "employee_id": self.john.id, + "check_in": check_in, + "check_out": check_out, + } + ) + + self.assertEqual( + att.day, + date(2022, 4, 3), + "Timezone with zero offset (2) from UTC has correct date", + ) + + def test_utc_and_tz_mismatch(self): + + att = self.Attendance.create( + { + "employee_id": self.sally.id, + "check_in": datetime(2022, 4, 1, 21, 0), + "check_out": datetime(2022, 4, 2, 5, 0), + } + ) + + self.assertEqual( + att.day, + date(2022, 4, 2), + "Timezone with +3:00 offset (1) from UTC has correct date", + ) + + def test_update(self): + + att = self.Attendance.create( + { + "employee_id": self.sally.id, + "check_in": datetime(2022, 4, 1, 21, 0), + "check_out": datetime(2022, 4, 2, 5, 0), + } + ) + + self.assertEqual( + att.day, + date(2022, 4, 2), + "Timezone with +3:00 offset (1) from UTC has correct date", + ) + + att.check_in = datetime(2022, 4, 1, 20, 59, 59) + self.assertEqual( + att.day, + date(2022, 4, 1), + "Changing check-in backwards by 1 second from midnight (in local tz) " + "changes the day", + ) diff --git a/hr_attendance_day/views/hr_attendance_view.xml b/hr_attendance_day/views/hr_attendance_view.xml new file mode 100644 index 00000000..8dbb4223 --- /dev/null +++ b/hr_attendance_day/views/hr_attendance_view.xml @@ -0,0 +1,26 @@ + + + + + hr.attendance.tree.payroll_policy_payslip + hr.attendance + + + + + + + + + + hr.attendance.form.payroll_policy_payslip + hr.attendance + + + + + + + + + diff --git a/hr_benefit/README.rst b/hr_benefit/README.rst new file mode 100644 index 00000000..0161d239 --- /dev/null +++ b/hr_benefit/README.rst @@ -0,0 +1,79 @@ +================== +Benefit Management +================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:6d8231e1e849f6c64514bb282ee18348de44836c96e61ad253f584952dbf1da7 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-trevi--software%2Ftrevi--hr-lightgray.png?logo=github + :target: https://github.com/trevi-software/trevi-hr/tree/14.0/hr_benefit + :alt: trevi-software/trevi-hr + +|badge1| |badge2| |badge3| + +Manage Employee Benefits +======================== +This module provides a comprehensive employee benefits management solution. +* Create benefits and their respective earnings and premiums +* Earnings and Premiums have effective dates to reflect changes over time +* The amounts in the benefit can be overriden in individual policies as necessary +* Benefits can be linked to payroll through the benefit code + +Some possible uses: +* Travel, Housing and other such allowances +* Employee personal medical expenses re-imbursement plans + +**Table of contents** + +.. contents:: + :local: + +Changelog +========= + +14.0.1.0.1 (2022-04-07) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [FIX] Creating a policy no longer fails because of a non-existent benefit + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* TREVI Software +* Michael Telahun Makonnen + +Other credits +~~~~~~~~~~~~~ + +* Michael Telahun Makonnen + +Maintainers +~~~~~~~~~~~ + +This module is part of the `trevi-software/trevi-hr `_ project on GitHub. + +You are welcome to contribute. diff --git a/hr_benefit/__init__.py b/hr_benefit/__init__.py new file mode 100644 index 00000000..91f9882b --- /dev/null +++ b/hr_benefit/__init__.py @@ -0,0 +1,6 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013,2014 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import models +from . import wizard diff --git a/hr_benefit/__manifest__.py b/hr_benefit/__manifest__.py new file mode 100644 index 00000000..36710535 --- /dev/null +++ b/hr_benefit/__manifest__.py @@ -0,0 +1,34 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013,2014 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + "name": "Benefit Management", + "summary": "Assign benefits and deductables to employees", + "version": "15.0.1.0.0", + "category": "Human Resources", + "author": "TREVI Software, Michael Telahun Makonnen", + "license": "AGPL-3", + "images": ["static/src/img/main_screenshot.png"], + "website": "https://github.com/trevi-software/trevi-hr", + "depends": [ + "hr", + "hr_contract_status", + "hr_employee_seniority_months", + ], + "data": [ + "security/ir.model.access.csv", + "security/security.xml", + "wizard/end_policy.xml", + "wizard/enroll_employee_view.xml", + "wizard/enroll_multi_employee_view.xml", + "views/benefit_view.xml", + "views/benefit_premium_view.xml", + "views/benefit_earning_view.xml", + "views/benefit_policy_view.xml", + "views/benefit_claim_view.xml", + "views/hr_employee_view.xml", + "data/benefit_sequence.xml", + ], + "installable": True, +} diff --git a/hr_benefit/data/benefit_sequence.xml b/hr_benefit/data/benefit_sequence.xml new file mode 100644 index 00000000..5a97ac7c --- /dev/null +++ b/hr_benefit/data/benefit_sequence.xml @@ -0,0 +1,11 @@ + + + + + Benefit Policy Reference + benefit.policy.ref + BP/%(year)s/ + 5 + + + diff --git a/hr_benefit/i18n/hr_benefit.pot b/hr_benefit/i18n/hr_benefit.pot new file mode 100644 index 00000000..64e61d04 --- /dev/null +++ b/hr_benefit/i18n/hr_benefit.pot @@ -0,0 +1,1047 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_benefit +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_day__10 +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_month_day__10 +msgid "10" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_day__11 +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_month_day__11 +msgid "11" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_day__12 +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_month_day__12 +msgid "12" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_day__13 +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_month_day__13 +msgid "13" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_day__14 +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_month_day__14 +msgid "14" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_day__15 +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_month_day__15 +msgid "15" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_day__16 +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_month_day__16 +msgid "16" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_day__17 +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_month_day__17 +msgid "17" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_day__18 +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_month_day__18 +msgid "18" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_day__19 +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_month_day__19 +msgid "19" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_day__20 +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_month_day__20 +msgid "20" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_day__21 +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_month_day__21 +msgid "21" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_day__22 +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_month_day__22 +msgid "22" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_day__23 +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_month_day__23 +msgid "23" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_day__24 +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_month_day__24 +msgid "24" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_day__25 +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_month_day__25 +msgid "25" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_day__26 +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_month_day__26 +msgid "26" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_day__27 +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_month_day__27 +msgid "27" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_day__28 +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_month_day__28 +msgid "28" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_day__29 +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_month_day__29 +msgid "29" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_day__30 +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_month_day__30 +msgid "30" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_day__31 +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_month_day__31 +msgid "31" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit__active +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_advantage__active +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__active +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_premium__active +msgid "Active" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefit_policy_filter +msgid "Active Policies" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_multi_employee__advantage_amount +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__advantage_amount +msgid "Advantage Amount" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__advantage_override_amount +msgid "Advantage Override Amount" +msgstr "" + +#. module: hr_benefit +#: model:ir.actions.act_window,name:hr_benefit.open_all_claims_view +#: model:ir.ui.menu,name:hr_benefit.menu_all_claims +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_all_claims_tree +msgid "All Claims" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__type__allowance +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefit_policy_advantage_form +msgid "Allowance" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_limit_period__annual +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_premium__type__annual +msgid "Annual" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,help:hr_benefit.field_hr_benefit_advantage__job_ids +msgid "Applies only to these job positions" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_all_claims_form +msgid "Approve" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_claim__state__approve +msgid "Approved" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_claim__amount_approved +msgid "Approved Amount" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_month__4 +msgid "April" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_month__8 +msgid "August" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_advantage__benefit_id +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_employee__benefit_id +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_multi_employee__benefit_id +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__benefit_id +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_premium__benefit_id +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefit_policy_filter +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefits_form +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefits_policy_form +msgid "Benefit" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefit_policy_advantage_form +msgid "Benefit Advantage" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefit_policy_advantage_tree +msgid "Benefit Advantages" +msgstr "" + +#. module: hr_benefit +#: model:ir.model,name:hr_benefit.model_hr_benefit_policy +msgid "Benefit Enrollment" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefits_policy_tree +msgid "Benefit Policies" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefits_policy_form +msgid "Benefit Policy" +msgstr "" + +#. module: hr_benefit +#: model:ir.model,name:hr_benefit.model_hr_benefit_policy_end +msgid "Benefit Policy Termination Wizard" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefit_policy_premium_form +msgid "Benefit Premium" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefit_policy_premium_tree +msgid "Benefit Premium Policies" +msgstr "" + +#. module: hr_benefit +#: model:ir.model,name:hr_benefit.model_hr_benefit_claim +msgid "Benefit claim" +msgstr "" + +#. module: hr_benefit +#: model:ir.actions.act_window,name:hr_benefit.act_hr_employee_2_hr_benefit_policy +#: model:ir.actions.act_window,name:hr_benefit.open_benefits_view +#: model:ir.model.fields,field_description:hr_benefit.field_hr_employee__benefit_policy_ids +#: model:ir.ui.menu,name:hr_benefit.menu_benefits +#: model:ir.ui.menu,name:hr_benefit.menu_benefits_configuration +#: model_terms:ir.ui.view,arch_db:hr_benefit.hr_employee_view_form +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefits_tree +msgid "Benefits" +msgstr "" + +#. module: hr_benefit +#: model:ir.ui.menu,name:hr_benefit.menu_benefits_root +msgid "Benefits Management" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__advantage_override +msgid "Change Advantage Amount" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__premium_override +msgid "Change Premium Amount" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,help:hr_benefit.field_hr_benefit_policy__advantage_override +msgid "" +"Check this field if the amount of the advantage should be changed in the " +"policy." +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,help:hr_benefit.field_hr_benefit_policy__premium_override +msgid "" +"Check this field if the amount of the premium should be changed in the " +"policy." +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_all_claims_form +msgid "Claim" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_employee__benefit_claims_count +#: model_terms:ir.ui.view,arch_db:hr_benefit.hr_employee_view_form +msgid "Claims" +msgstr "" + +#. module: hr_benefit +#: model:ir.ui.menu,name:hr_benefit.menu_claims_root +msgid "Claims & Payments" +msgstr "" + +#. module: hr_benefit +#: model:ir.actions.act_window,name:hr_benefit.act_hr_employee_2_hr_benefit_claims +msgid "Claims on Benefits" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit__code +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__benefit_code +msgid "Code" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit__company_id +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__company_id +msgid "Company" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit__create_uid +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_advantage__create_uid +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_claim__create_uid +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_employee__create_uid +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_multi_employee__create_uid +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__create_uid +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy_end__create_uid +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_premium__create_uid +msgid "Created by" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit__create_date +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_advantage__create_date +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_claim__create_date +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_employee__create_date +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_multi_employee__create_date +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__create_date +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy_end__create_date +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_premium__create_date +msgid "Created on" +msgstr "" + +#. module: hr_benefit +#: code:addons/hr_benefit/models/benefit_policy.py:0 +#, python-format +msgid "Critical Error. Unable to obtain a benefit policy number!" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefit_policy_filter +msgid "Current Policies" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_claim__date +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_claims_filter +msgid "Date" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__start_date +msgid "Date of Enrollment" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_advantage__reim_period_annual_day +msgid "Day of Month" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_month__12 +msgid "December" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_all_claims_form +msgid "Decline" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_claim__state__decline +msgid "Declined" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_advantage__allowance_amount +msgid "Default Amount" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__department_id +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefit_policy_filter +msgid "Department" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefits_policy_form +msgid "Details" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit__display_name +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_advantage__display_name +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_claim__display_name +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_employee__display_name +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_multi_employee__display_name +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__display_name +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy_end__display_name +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_premium__display_name +#: model:ir.model.fields,field_description:hr_benefit.field_hr_employee__display_name +msgid "Display Name" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_policy__state__done +msgid "Done" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_claim__state__draft +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_policy__state__draft +msgid "Draft" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefits_policy_form +msgid "Duration" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_advantage__type +msgid "Earning Type" +msgstr "" + +#. module: hr_benefit +#: model:ir.actions.act_window,name:hr_benefit.open_benefits_policy_advantage_view +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit__advantage_ids +#: model:ir.ui.menu,name:hr_benefit.menu_benefits_policy_advantage +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefits_form +msgid "Earnings" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_advantage__effective_date +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_premium__effective_date +msgid "Effective Date" +msgstr "" + +#. module: hr_benefit +#: code:addons/hr_benefit/models/benefit_allwance.py:0 +#: model:ir.model.constraint,message:hr_benefit.constraint_hr_benefit_advantage_unique_date_benefit_id +#, python-format +msgid "Effective date must be unique per advantage in a benefit!" +msgstr "" + +#. module: hr_benefit +#: code:addons/hr_benefit/models/benefit_premium.py:0 +#: model:ir.model.constraint,message:hr_benefit.constraint_hr_benefit_premium_unique_date_benefit_id +#, python-format +msgid "Effective dates must be unique per premium in a benefit!" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefit_policy_advantage_form +msgid "Elegibility" +msgstr "" + +#. module: hr_benefit +#: code:addons/hr_benefit/models/benefit_policy.py:0 +#, python-format +msgid "" +"Eligibility Requirements Unmet. The employee does not meet eligibility " +"requirements for this benefit." +msgstr "" + +#. module: hr_benefit +#: model:ir.model,name:hr_benefit.model_hr_employee +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_claim__employee_id +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_employee__employee_id +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_multi_employee__employee_ids +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__employee_id +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefit_policy_filter +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_claims_filter +msgid "Employee" +msgstr "" + +#. module: hr_benefit +#: model:ir.model,name:hr_benefit.model_hr_benefit +msgid "Employee Benefit" +msgstr "" + +#. module: hr_benefit +#: model:ir.model,name:hr_benefit.model_hr_benefit_enroll_employee +#: model:ir.model,name:hr_benefit.model_hr_benefit_enroll_multi_employee +msgid "Employee Benefit Enrollment Form" +msgstr "" + +#. module: hr_benefit +#: model:ir.model,name:hr_benefit.model_hr_benefit_advantage +msgid "Employee Benefit Policy Earning Line" +msgstr "" + +#. module: hr_benefit +#: model:ir.model,name:hr_benefit.model_hr_benefit_premium +msgid "Employee Benefit Premium Policy Line" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_advantage__category_ids +msgid "Employee Categories" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.enroll_multi_employee_form +msgid "Employees" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.end_benefit_policy_form +msgid "End Benefit Policy" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy_end__date +msgid "End Date" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefits_policy_form +msgid "End Policy" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.enroll_employee_form +#: model_terms:ir.ui.view,arch_db:hr_benefit.enroll_multi_employee_form +msgid "Enroll" +msgstr "" + +#. module: hr_benefit +#: model:ir.actions.act_window,name:hr_benefit.action_enroll_employee +#: model:ir.actions.server,name:hr_benefit.server_action_enroll_employee +#: model_terms:ir.ui.view,arch_db:hr_benefit.enroll_employee_form +#: model_terms:ir.ui.view,arch_db:hr_benefit.enroll_multi_employee_form +msgid "Enroll Employee" +msgstr "" + +#. module: hr_benefit +#: model:ir.actions.act_window,name:hr_benefit.action_enroll_multiemployee +#: model:ir.actions.server,name:hr_benefit.server_action_enroll_multiemployee +msgid "Enroll Multiple Employees" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_employee__start_date +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_multi_employee__start_date +msgid "Enrollment Date" +msgstr "" + +#. module: hr_benefit +#: code:addons/hr_benefit/models/benefit_claim.py:0 +#, python-format +msgid "ErrorYou may not a delete a claim that is not in a \"Draft\" state" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_advantage__invert_categories +msgid "Exclude Categories" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_advantage__invert_jobs +msgid "Exclude Jobs" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__type__reimburse +msgid "Expense Reimbursement" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_month__2 +msgid "February" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefit_policy_filter +msgid "Filters policies that are not terminated." +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.end_benefit_policy_form +msgid "Finish" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_advantage__reim_period_month_day +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefit_policy_advantage_form +msgid "First Day of Cycle" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefits_policy_form +msgid "General" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefit_policy_filter +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_claims_filter +msgid "Group By..." +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit__has_advantage +msgid "Has Advantage" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit__has_premium +msgid "Has Premium" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit__id +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_advantage__id +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_claim__id +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_employee__id +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_multi_employee__id +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__id +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy_end__id +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_premium__id +#: model:ir.model.fields,field_description:hr_benefit.field_hr_employee__id +msgid "ID" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,help:hr_benefit.field_hr_benefit_advantage__allowance_amount +msgid "" +"If the allowance is not calculated in the salary rule this is the amount of " +"the allowance" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,help:hr_benefit.field_hr_benefit_advantage__invert_categories +msgid "" +"If this is checked invert the sense of the match for the categories list. " +"Exclude employees in the selected categories." +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,help:hr_benefit.field_hr_benefit_advantage__invert_jobs +msgid "" +"If this is checked invert the sense of the match for the jobs list. Exclude " +"employees in the selected jobs." +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_advantage__job_ids +msgid "Included Job Positions" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_month__1 +msgid "January" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_month__7 +msgid "July" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_month__6 +msgid "June" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit____last_update +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_advantage____last_update +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_claim____last_update +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_employee____last_update +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_multi_employee____last_update +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy____last_update +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy_end____last_update +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_premium____last_update +#: model:ir.model.fields,field_description:hr_benefit.field_hr_employee____last_update +msgid "Last Modified on" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit__write_uid +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_advantage__write_uid +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_claim__write_uid +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_employee__write_uid +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_multi_employee__write_uid +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__write_uid +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy_end__write_uid +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_premium__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit__write_date +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_advantage__write_date +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_claim__write_date +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_employee__write_date +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_multi_employee__write_date +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__write_date +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy_end__write_date +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_premium__write_date +msgid "Last Updated on" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefit_policy_advantage_form +msgid "Limit" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_advantage__reim_limit_amount +msgid "Limit Amount" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_advantage__reim_limit_period +msgid "Limit Period" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__type__loan +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefit_policy_advantage_form +msgid "Loan" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_advantage__loan_amount +msgid "Loan Amount" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_month__3 +msgid "March" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_month__5 +msgid "May" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit__min_employed_days +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_advantage__min_employed_days +msgid "Minimum Employed Days" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_advantage__reim_period_annual_month +msgid "Month" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_limit_period__monthly +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_premium__type__monthly +msgid "Monthly" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit__multi_policy +msgid "Multiple Policies/Employee" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_claims_filter +msgid "My Claims" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefit_policy_filter +msgid "My Policies" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit__name +msgid "Name" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_advantage__reim_nolimit +msgid "No Limit" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_multi_employee__premium_installments +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__premium_installments +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_premium__no_of_installments +msgid "No. of Installments" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_month__11 +msgid "November" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,help:hr_benefit.field_hr_benefit_advantage__min_employed_days +msgid "" +"Number of days of employment before employee is eligible for this advantage." +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_month__10 +msgid "October" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_policy__state__open +msgid "Open" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_multi_employee__advantage_override +msgid "Override Advantage" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_multi_employee__premium_override +msgid "Override Premium" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.end_benefit_policy_form +msgid "Please enter the last day of the policy in the field provided below." +msgstr "" + +#. module: hr_benefit +#: model:ir.actions.act_window,name:hr_benefit.open_benefits_policy_view +#: model:ir.actions.act_window,name:hr_benefit.open_my_benefit_policies +#: model:ir.model.fields,field_description:hr_benefit.field_hr_employee__benefit_policies_count +#: model:ir.ui.menu,name:hr_benefit.menu_my_benefit_policies +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefit_policy_filter +msgid "Policies" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_claim__benefit_policy_id +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_claims_filter +msgid "Policy" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefits_form +msgid "Premium" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_multi_employee__premium_amount +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__premium_amount +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_premium__amount +msgid "Premium Amount" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__premium_override_amount +msgid "Premium Override Amount" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__premium_override_total +msgid "Premium Override Total" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_multi_employee__premium_total +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__premium_total +msgid "Premium Total" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_premium__type +msgid "Premium Type" +msgstr "" + +#. module: hr_benefit +#: model:ir.actions.act_window,name:hr_benefit.open_benefits_policy_premium_view +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit__premium_ids +#: model:ir.ui.menu,name:hr_benefit.menu_benefits_policy_premium +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefits_form +msgid "Premiums" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefit_policy_advantage_form +msgid "Re-imbursement" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__name +msgid "Reference" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__note +msgid "Remarks" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_claim__amount_requested +msgid "Requested Amount" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_all_claims_form +msgid "Reset to New" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_claims_filter +msgid "Search All Claims" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefit_policy_filter +msgid "Search Benefit Policies" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields.selection,name:hr_benefit.selection__hr_benefit_advantage__reim_period_annual_month__9 +msgid "September" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefit_policy_filter +msgid "Start Date" +msgstr "" + +#. module: hr_benefit +#: model_terms:ir.ui.view,arch_db:hr_benefit.view_benefits_policy_form +msgid "Start Policy" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_claim__state +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__state +msgid "State" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_employee__end_date +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_enroll_multi_employee__end_date +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_policy__end_date +msgid "Termination Date" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,help:hr_benefit.field_hr_benefit_advantage__loan_amount +msgid "The amount advanced to the employee" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,help:hr_benefit.field_hr_benefit_claim__date +msgid "The date the claim was made" +msgstr "" + +#. module: hr_benefit +#: code:addons/hr_benefit/models/benefit_policy.py:0 +#, python-format +msgid "" +"The employee is already enrolled in this benefit program.\n" +"%s\n" +"Policy: %s" +msgstr "" + +#. module: hr_benefit +#: model:ir.model.fields,field_description:hr_benefit.field_hr_benefit_premium__total_amount +msgid "Total" +msgstr "" + +#. module: hr_benefit +#: code:addons/hr_benefit/wizard/end_policy.py:0 +#, python-format +msgid "Unable to determine Benefit Policy." +msgstr "" + +#. module: hr_benefit +#: code:addons/hr_benefit/models/benefit_allwance.py:0 +#, python-format +msgid "" +"Wrong earning type for this operation. Use 'Expense Re-imbursement' instead." +msgstr "" + +#. module: hr_benefit +#: code:addons/hr_benefit/models/benefit_policy.py:0 +#, python-format +msgid "You cannot set a policy in 'Done' state to any other value." +msgstr "" + +#. module: hr_benefit +#: code:addons/hr_benefit/models/benefit_claim.py:0 +#, python-format +msgid "You cannot set an approved claim back to 'draft' state." +msgstr "" + +#. module: hr_benefit +#: code:addons/hr_benefit/models/benefit_policy.py:0 +#: code:addons/hr_benefit/models/benefit_policy.py:0 +#, python-format +msgid "You cannot set an open policy back to 'draft' state." +msgstr "" + +#. module: hr_benefit +#: code:addons/hr_benefit/models/benefit_policy.py:0 +#, python-format +msgid "" +"You may not delete a policy that is not in a \"draft\" state.\n" +"Policy No: %s" +msgstr "" diff --git a/hr_benefit/models/__init__.py b/hr_benefit/models/__init__.py new file mode 100644 index 00000000..9e1aff28 --- /dev/null +++ b/hr_benefit/models/__init__.py @@ -0,0 +1,10 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013,2014 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import benefit +from . import benefit_allwance +from . import benefit_claim +from . import benefit_policy +from . import benefit_premium +from . import hr_employee diff --git a/hr_benefit/models/benefit.py b/hr_benefit/models/benefit.py new file mode 100644 index 00000000..0e66008d --- /dev/null +++ b/hr_benefit/models/benefit.py @@ -0,0 +1,85 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013,2014 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class Benefit(models.Model): + + _name = "hr.benefit" + _description = "Employee Benefit" + _check_company_auto = True + + name = fields.Char(required=True) + code = fields.Char(required=True) + has_premium = fields.Boolean() + premium_ids = fields.One2many( + string="Premiums", comodel_name="hr.benefit.premium", inverse_name="benefit_id" + ) + has_advantage = fields.Boolean() + advantage_ids = fields.One2many( + string="Earnings", + comodel_name="hr.benefit.advantage", + inverse_name="benefit_id", + ) + min_employed_days = fields.Integer(string="Minimum Employed Days", default=0) + active = fields.Boolean(default=True) + multi_policy = fields.Boolean(string="Multiple Policies/Employee", default=False) + company_id = fields.Many2one( + "res.company", required=True, default=lambda self: self.env.company + ) + + def name_get(self): + + res = [] + for rec in self: + res.append((rec.id, "[%s] %s" % (rec.code, rec.name))) + + return res + + def _get_latest(self, dToday, ptype): + """ + Return an advantage with an effective date before dToday but + greater than all others. + """ + + if not self or not dToday: + return None + self.ensure_one() + + res = None + line_ids = None + if ptype == "advantage": + line_ids = self.advantage_ids + elif ptype == "premium": + line_ids = self.premium_ids + + for line in line_ids: + dLine = line.effective_date + if dLine <= dToday: + if res is None: + res = line + elif dLine > res.effective_date: + res = line + + return res + + def get_latest_advantage(self, dToday): + """ + Return an advantage with an effective date before dToday but + greater than all others. + """ + + if not dToday: + return None + + return self._get_latest(dToday, "advantage") + + def get_latest_premium(self, dToday): + """Return a premium with an effective date before dToday but greater than all others""" + + if not dToday: + return None + + return self._get_latest(dToday, "premium") diff --git a/hr_benefit/models/benefit_allwance.py b/hr_benefit/models/benefit_allwance.py new file mode 100644 index 00000000..34c9fcaf --- /dev/null +++ b/hr_benefit/models/benefit_allwance.py @@ -0,0 +1,237 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013,2014 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from datetime import timedelta + +from dateutil.relativedelta import relativedelta + +from odoo import _, fields, models +from odoo.exceptions import UserError + +DAY_SELECT = [ + ("1", "1"), + ("2", "2"), + ("3", "3"), + ("4", "4"), + ("5", "5"), + ("6", "6"), + ("7", "7"), + ("8", "8"), + ("9", "9"), + ("10", "10"), + ("11", "11"), + ("12", "12"), + ("13", "13"), + ("14", "14"), + ("15", "15"), + ("16", "16"), + ("17", "17"), + ("18", "18"), + ("19", "19"), + ("20", "20"), + ("21", "21"), + ("22", "22"), + ("23", "23"), + ("24", "24"), + ("25", "25"), + ("26", "26"), + ("27", "27"), + ("28", "28"), + ("29", "29"), + ("30", "30"), + ("31", "31"), +] + + +class BenefitAdvantage(models.Model): + + _name = "hr.benefit.advantage" + _description = "Employee Benefit Policy Earning Line" + _rec_name = "effective_date" + _order = "benefit_id,effective_date desc" + _sql_constraints = [ + ( + "unique_date_benefit_id", + "UNIQUE(effective_date,benefit_id)", + _("Effective date must be unique per advantage in a benefit!"), + ) + ] + + benefit_id = fields.Many2one(string="Benefit", comodel_name="hr.benefit") + effective_date = fields.Date(required=True) + min_employed_days = fields.Integer( + string="Minimum Employed Days", + help="Number of days of employment before employee is eligible for this advantage.", + ) + type = fields.Selection( + string="Earning Type", + selection=[ + ("allowance", "Allowance"), + ("reimburse", "Expense Reimbursement"), + ("loan", "Loan"), + ], + required=True, + ) + allowance_amount = fields.Float( + string="Default Amount", + digits="Account", + help="If the allowance is not calculated in the salary " + "rule this is the amount of the allowance", + ) + reim_nolimit = fields.Boolean(string="No Limit") + reim_limit_amount = fields.Float(string="Limit Amount", digits="Account") + reim_limit_period = fields.Selection( + string="Limit Period", + selection=[ + ("monthly", "Monthly"), + ("annual", "Annual"), + ], + ) + reim_period_month_day = fields.Selection( + string="First Day of Cycle", + selection=DAY_SELECT, + ) + reim_period_annual_month = fields.Selection( + string="Month", + selection=[ + ("1", "January"), + ("2", "February"), + ("3", "March"), + ("4", "April"), + ("5", "May"), + ("6", "June"), + ("7", "July"), + ("8", "August"), + ("9", "September"), + ("10", "October"), + ("11", "November"), + ("12", "December"), + ], + ) + reim_period_annual_day = fields.Selection( + string="Day of Month", + selection=DAY_SELECT, + ) + loan_amount = fields.Float( + digits="Payroll", help="The amount advanced to the employee" + ) + category_ids = fields.Many2many( + string="Employee Categories", + comodel_name="hr.employee.category", + relation="benefit_advantage_category_rel", + column1="advantage_id", + column2="category_id", + ) + job_ids = fields.Many2many( + string="Included Job Positions", + comodel_name="hr.job", + relation="benefit_advantage_job_rel", + column1="advantage_id", + column2="job_id", + help="Applies only to these job positions", + ) + invert_categories = fields.Boolean( + string="Exclude Categories", + help="If this is checked invert the sense of the " + "match for the categories list. Exclude employees " + "in the selected categories.", + ) + invert_jobs = fields.Boolean( + string="Exclude Jobs", + help="If this is checked invert the sense of the " + "match for the jobs list. Exclude employees in " + "the selected jobs.", + ) + active = fields.Boolean(default=True) + + def name_get(self): + res = [] + for rec in self: + res.append( + (rec.id, "{} {}".format(rec.benefit_id.name, rec.effective_date)) + ) + return res + + def get_claims_in_period(self, employee_id, day): + + d = day + period = self.reim_limit_period + if period == "monthly": + diff = d.day - int(self.reim_period_month_day) + if diff == 0: + dStart = d + elif diff > 0: + dStart = d + timedelta(days=-(diff)) + else: + dStart = d + relativedelta(months=-1, days=diff) + dNextStart = dStart + relativedelta(months=+1) + elif period == "annual": + day_diff = d.day - int(self.reim_period_annual_day) + month_diff = d.month - int(self.reim_period_annual_month) + if month_diff == 0: + dStart = d + elif month_diff > 0: + dStart = d + relativedelta(months=-(month_diff)) + else: + # month_diff is negative, but: -(-) = + + dStart = d + relativedelta(months=-(month_diff)) + if day_diff > 0: + dStart = dStart + timedelta(days=-(day_diff)) + elif day_diff < 0: + dStart = dStart + relativedelta(months=-1, days=day_diff) + dNextStart = dStart + relativedelta(years=+1) + else: + return 0.00 + + claim_obj = self.env["hr.benefit.claim"] + claim_ids = claim_obj.search( + [ + ("employee_id", "=", employee_id.id), + ("benefit_policy_id.benefit_id", "=", self.benefit_id.id), + ("benefit_policy_id.start_date", "<", dNextStart), + "|", + ("benefit_policy_id.end_date", "=", False), + ("benefit_policy_id.end_date", ">=", dStart), + ("date", ">=", dStart), + ("date", "<", dNextStart), + ("state", "=", "approve"), + ] + ) + res = 0.00 + if len(claim_ids) > 0: + self.env.cr.execute( + "SELECT SUM(amount_approved) FROM hr_benefit_claim " "WHERE id in %s", + (tuple(claim_ids.ids),), + ) + res = self.env.cr.fetchall()[0][0] + return res + + def get_reimburse_remaining(self, employee_id, day): + + self.ensure_one() + if self.type != "reimburse": + raise UserError( + _( + "Wrong earning type for this operation. " + "Use 'Expense Re-imbursement' instead." + ) + ) + policies = self.env["hr.benefit.policy"].search( + [ + ("benefit_id", "=", self.benefit_id.id), + ("employee_id", "=", employee_id.id), + "|", + ("end_date", "=", False), + ("end_date", ">=", day), + ] + ) + if len(policies) == 0: + res = 0.0 + elif self.reim_nolimit: + res = 0.0 + elif self.reim_limit_period: + claims = self.get_claims_in_period(employee_id, day) + unclaimed = self.reim_limit_amount - claims + res = (unclaimed < 0.01) and 0.00 or unclaimed + return res diff --git a/hr_benefit/models/benefit_claim.py b/hr_benefit/models/benefit_claim.py new file mode 100644 index 00000000..e884353d --- /dev/null +++ b/hr_benefit/models/benefit_claim.py @@ -0,0 +1,167 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013,2014 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class BenefitClaim(models.Model): + + _name = "hr.benefit.claim" + _description = "Benefit claim" + _rec_name = "date" + + date = fields.Date( + required=True, + readonly=True, + states={"draft": [("readonly", False)]}, + help="The date the claim was made", + default=fields.Date.today(), + ) + benefit_policy_id = fields.Many2one( + string="Policy", + required=True, + comodel_name="hr.benefit.policy", + readonly=True, + states={"draft": [("readonly", False)]}, + ) + employee_id = fields.Many2one( + string="Employee", + comodel_name="hr.employee", + required=True, + readonly=True, + states={"draft": [("readonly", False)]}, + ) + amount_requested = fields.Float( + string="Requested Amount", + digits="Account", + required=True, + readonly=True, + states={"draft": [("readonly", False)]}, + ) + amount_approved = fields.Float( + string="Approved Amount", digits="Account", readonly=True + ) + state = fields.Selection( + selection=[ + ("draft", "Draft"), + ("approve", "Approved"), + ("decline", "Declined"), + ], + readonly=True, + default="draft", + ) + + def name_get(self): + + res = [] + data = self.read(["date", "benefit_policy_id"]) + for d in data: + res.append((d["id"], d["benefit_policy_id"][1] + " " + d["date"])) + return res + + @api.onchange("employee_id") + def onchange_employee(self): + + res = {"domain": {"benefit_policy_id": False}} + if not self.employee_id: + return res + + res["domain"]["benefit_policy_id"] = [ + ("employee_id", "=", self.employee_id.id), + ("benefit_id.has_advantage", "=", True), + ] + return res + + def _get_approved_amount(self, dDate): + + self.ensure_one() + approved = 0.00 + if dDate: + policy = self.benefit_policy_id + d = dDate + adv_line = policy.benefit_id.get_latest_advantage(d) + if adv_line: + remaining = adv_line.get_reimburse_remaining(self.employee_id, dDate) + if adv_line.reim_nolimit: + # No limit + approved = self.amount_requested + elif -1 == fields.Float.compare(remaining, 0.0, precision_digits=2): + # Over Limit + approved = 0.00 + else: + if -1 == fields.Float.compare( + remaining, self.amount_requested, precision_digits=2 + ): + approved = remaining + else: + approved = self.amount_requested + + return approved + + @api.model + def create(self, vals): + + res = super(BenefitClaim, self).create(vals) + for rec in res: + approved = res._get_approved_amount(vals["date"]) + rec.amount_approved = approved + + return res + + def _check_state(self, to_state): + if self.state == "approve" and to_state == "draft": + raise UserError( + _("You cannot set an approved claim back to 'draft' state.") + ) + + def write(self, vals): + + _fields = ["date", "employee_id", "amount_requested", "benefit_policy_id"] + do_calc = False + for k in vals: + if k in _fields: + do_calc = True + break + + if "state" in vals.keys(): + for clm in self: + clm._check_state(vals["state"]) + + res = super(BenefitClaim, self).write(vals) + if do_calc: + for clm in self: + approved = self._get_approved_amount(vals.get("date")) + clm.amount_approved = approved + + return res + + def unlink(self): + + data = self.read(["state"]) + for d in data: + if d["state"] in ["approve", "decline"] and not ( + self.env.context and self.env.context.get("force_delete", False) + ): + raise UserError( + _( + "Error" + 'You may not a delete a claim that is not in a "Draft" state' + ) + ) + return super(BenefitClaim, self).unlink() + + def set_to_draft(self): + self.state = "draft" + return True + + def claim_approve(self): + + self.write({"state": "approve"}) + return True + + def claim_decline(self): + + self.write({"state": "decline"}) + return True diff --git a/hr_benefit/models/benefit_policy.py b/hr_benefit/models/benefit_policy.py new file mode 100644 index 00000000..2c12d918 --- /dev/null +++ b/hr_benefit/models/benefit_policy.py @@ -0,0 +1,300 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013,2014 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import math +from datetime import date, datetime, timedelta + +from dateutil.relativedelta import relativedelta + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class BenefitPolicy(models.Model): + + _name = "hr.benefit.policy" + _description = "Benefit Enrollment" + _check_company_auto = True + + name = fields.Char(string="Reference", readonly=True) + benefit_id = fields.Many2one( + string="Benefit", + comodel_name="hr.benefit", + required=True, + readonly=True, + states={"draft": [("readonly", False)]}, + check_company=True, + ) + benefit_code = fields.Char(readonly=True, related="benefit_id.code", store=True) + employee_id = fields.Many2one( + string="Employee", + comodel_name="hr.employee", + required=True, + readonly=True, + states={"draft": [("readonly", False)]}, + check_company=True, + ) + department_id = fields.Many2one(related="employee_id.department_id", store=True) + start_date = fields.Date( + string="Date of Enrollment", + required=True, + readonly=True, + states={"draft": [("readonly", False)]}, + ) + end_date = fields.Date(string="Termination Date") + active = fields.Boolean(default=True) + advantage_override = fields.Boolean( + string="Change Advantage Amount", + help="Check this field if the amount of the advantage should be changed in the policy.", + ) + premium_override = fields.Boolean( + string="Change Premium Amount", + help="Check this field if the amount of the premium should be changed in the policy.", + ) + advantage_override_amount = fields.Float(digits="Account") + premium_override_amount = fields.Float(digits="Account") + premium_override_total = fields.Float(digits="Account") + advantage_amount = fields.Float( + digits="Account", + compute="_compute_amounts", + inverse="_inverse_amounts", + store=True, + ) + premium_amount = fields.Float( + digits="Account", + compute="_compute_amounts", + inverse="_inverse_amounts", + store=True, + ) + premium_total = fields.Float( + digits="Account", + compute="_compute_amounts", + inverse="_inverse_amounts", + store=True, + ) + premium_installments = fields.Integer( + string="No. of Installments", + compute="_compute_premium_installments", + store=True, + ) + note = fields.Text(string="Remarks") + state = fields.Selection( + selection=[("draft", "Draft"), ("open", "Open"), ("done", "Done")], + readonly=True, + default="draft", + ) + company_id = fields.Many2one( + "res.company", required=True, default=lambda self: self.env.company + ) + + def name_get(self): + res = [] + for rec in self: + res.append((rec.id, "{} {}".format(rec.name, rec.benefit_id.name))) + return res + + @api.depends( + "benefit_id", + "advantage_override", + "premium_override", + "advantage_override_amount", + "premium_override_amount", + "premium_override_total", + ) + def _compute_amounts(self): + + for rec in self: + res = { + "advantage_amount": 0, + "premium_amount": 0, + "premium_total": 0, + } + + if not rec.benefit_id or not rec.start_date: + rec.write(res) + continue + + dToday = date.today() + if rec.advantage_override: + res["advantage_amount"] = rec.advantage_override_amount + else: + adv = rec.benefit_id.get_latest_advantage(dToday) + if adv is not None: + if adv.type == "allowance": + res["advantage_amount"] = adv.allowance_amount + if rec.premium_override: + res["premium_amount"] = rec.premium_override_amount + res["premium_total"] = rec.premium_override_total + else: + prm = rec.benefit_id.get_latest_premium(dToday) + if prm is not None: + res["premium_amount"] = prm.amount + res["premium_total"] = prm.total_amount + + rec.write(res) + + def _inverse_amounts(self): + for rec in self: + if rec.advantage_override: + rec.advantage_override_amount = rec.advantage_amount + if rec.premium_override: + rec.premium_override_amount = rec.premium_amount + rec.premium_override_total = rec.premium_total + + @api.depends("start_date", "premium_amount", "premium_total") + def _compute_premium_installments(self): + + for rec in self: + res = {"end_date": False, "premium_installments": 0} + if rec.premium_amount == 0: + rec.write(res) + return + + installments = int( + math.ceil(float(rec.premium_total) / float(rec.premium_amount)) + ) + if installments > 0: + dEnd = rec.start_date + relativedelta(months=+installments) + res["end_date"] = dEnd + res["premium_installments"] = installments + rec.write(res) + + @api.model + def _fail_eligibility(self, benefit_id, employee_id): + + res = False + benefit = self.env["hr.benefit"].browse(benefit_id) + + # Check if employee has worked more than minimum number of days for benefit + # + if benefit.min_employed_days > 0: + ee = self.env["hr.employee"].browse(employee_id) + dToday = datetime.today().date() + dHire = ee.first_contract_date + srvc_months = ee.get_months_service_to_date(dToday=dToday) + srvc_months = int(srvc_months) + + employed_days = 0 + dCount = dHire + while dCount < dToday: + employed_days += 1 + dCount += timedelta(days=+1) + if benefit.min_employed_days > employed_days: + res = True + + return res + + @api.model + def create(self, vals): + + # Check if the employee is already enrolled + # + benefit = self.env["hr.benefit"].browse(vals["benefit_id"]) + if not benefit.multi_policy: + domain = [ + ("employee_id", "=", vals["employee_id"]), + ("benefit_id", "=", vals["benefit_id"]), + ] + if vals["start_date"] and not vals.get("end_date", False): + domain = domain + [ + "|", + ("end_date", "=", False), + ("end_date", ">=", vals["start_date"]), + ] + elif vals["start_date"] and vals["end_date"]: + domain = domain + [ + ("start_date", "<=", vals["end_date"]), + "|", + ("end_date", "=", False), + ("end_date", ">=", vals["start_date"]), + ] + + policy_ids = self.search(domain) + if len(policy_ids) > 0: + raise UserError( + _( + "The employee is already enrolled in this benefit program." + "\n%s\nPolicy: %s" + ) + % (policy_ids[0].employee_id.name, policy_ids[0].name) + ) + + # Check if eligibility requirements have been met + if self._fail_eligibility(vals["benefit_id"], vals["employee_id"]): + raise UserError( + _( + "Eligibility Requirements Unmet. " + "The employee does not meet eligibility requirements for this benefit." + ) + ) + + ben_id = super(BenefitPolicy, self).create(vals) + if ben_id: + ref = self.env["ir.sequence"].next_by_code("benefit.policy.ref") + if not ref: + raise UserError( + _("Critical Error. " "Unable to obtain a benefit policy number!") + ) + ben_id.name = ref + return ben_id + + def unlink(self): + + for pol in self: + if pol.state != "draft" and not ( + self.env.context and self.env.context.get("force_delete", False) + ): + raise UserError( + _( + 'You may not delete a policy that is not in a "draft" state.' + "\nPolicy No: %s" % (pol.name) + ) + ) + + return super(BenefitPolicy, self).unlink() + + def _check_state(self, to_state): + for rec in self: + if to_state == "draft" and rec.state not in ["", "draft"]: + raise UserError( + _("You cannot set an open policy back to 'draft' state.") + ) + elif to_state == "done" and rec.state != "open": + raise UserError( + _("You cannot set an open policy back to 'draft' state.") + ) + elif rec.state == "done": + raise UserError( + _("You cannot set a policy in 'Done' state to any other value.") + ) + + def write(self, vals): + if "state" in vals: + self._check_state(vals["state"]) + return super(BenefitPolicy, self).write(vals) + + def state_open(self): + + self.write({"state": "open"}) + return True + + def state_done(self): + + self.write({"state": "done"}) + return True + + def end_policy(self): + + if len(self.ids) == 0: + return False + + self.env.context.update({"end_benefit_policy_id": self.ids[0]}) + return { + "view_type": "form", + "view_mode": "form", + "res_model": "hr.benefit.policy.end", + "type": "ir.actions.act_window", + "target": "new", + "context": self.env.context, + } diff --git a/hr_benefit/models/benefit_premium.py b/hr_benefit/models/benefit_premium.py new file mode 100644 index 00000000..99951e2a --- /dev/null +++ b/hr_benefit/models/benefit_premium.py @@ -0,0 +1,56 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013,2014 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import math + +from odoo import _, api, fields, models + + +class BenefitPremium(models.Model): + + _name = "hr.benefit.premium" + _description = "Employee Benefit Premium Policy Line" + _rec_name = "effective_date" + _order = "benefit_id,effective_date desc" + _sql_constraints = [ + ( + "unique_date_benefit_id", + "UNIQUE(effective_date,benefit_id)", + _("Effective dates must be unique per premium in a benefit!"), + ) + ] + + benefit_id = fields.Many2one(string="Benefit", comodel_name="hr.benefit") + effective_date = fields.Date(required=True) + type = fields.Selection( + string="Premium Type", + selection=[("monthly", "Monthly"), ("annual", "Annual")], + required=True, + ) + amount = fields.Float(string="Premium Amount", digits="Account") + total_amount = fields.Float(string="Total", digits="Account") + no_of_installments = fields.Integer( + string="No. of Installments", + compute="_compute_installments", + store=True, + default=0, + ) + active = fields.Boolean(default=True) + + def name_get(self): + res = [] + for rec in self: + res.append( + (rec.id, "{} {}".format(rec.benefit_id.name, rec.effective_date)) + ) + return res + + @api.depends("amount", "total_amount") + def _compute_installments(self): + for prm in self: + prm.no_of_installments = ( + (prm.amount > 0 and prm.total_amount > 0) + and int(math.ceil(float(prm.total_amount) / float(prm.amount))) + or 0 + ) diff --git a/hr_benefit/models/hr_employee.py b/hr_benefit/models/hr_employee.py new file mode 100644 index 00000000..d35838ab --- /dev/null +++ b/hr_benefit/models/hr_employee.py @@ -0,0 +1,48 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013,2014 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from datetime import date + +from odoo import fields, models + + +class HrEmployee(models.Model): + + _inherit = "hr.employee" + + benefit_policy_ids = fields.One2many( + string="Benefits", comodel_name="hr.benefit.policy", inverse_name="employee_id" + ) + benefit_policies_count = fields.Integer( + string="Policies", compute="_compute_policies_count" + ) + benefit_claims_count = fields.Integer( + string="Claims", compute="_compute_claims_count" + ) + + def _compute_policies_count(self): + HrBenefitPolicy = self.env["hr.benefit.policy"] + dToday = date.today() + for ee in self: + ee.benefit_policies_count = HrBenefitPolicy.search_count( + [ + ("id", "in", ee.benefit_policy_ids.ids), + ("employee_id", "=", ee.id), + ("start_date", "<=", dToday), + ("state", "!=", "done"), + "|", + ("end_date", "=", False), + ("end_date", ">=", dToday), + ] + ) + + def _compute_claims_count(self): + Claim = self.env["hr.benefit.claim"] + for ee in self: + ee.benefit_claims_count = Claim.search_count( + [ + ("employee_id", "=", ee.id), + ("state", "!=", "decline"), + ] + ) diff --git a/hr_benefit/readme/CREDITS.rst b/hr_benefit/readme/CREDITS.rst new file mode 100644 index 00000000..d264bc7e --- /dev/null +++ b/hr_benefit/readme/CREDITS.rst @@ -0,0 +1 @@ +* Michael Telahun Makonnen diff --git a/hr_benefit/readme/DESCRIPTION.rst b/hr_benefit/readme/DESCRIPTION.rst new file mode 100644 index 00000000..91fddeb9 --- /dev/null +++ b/hr_benefit/readme/DESCRIPTION.rst @@ -0,0 +1,11 @@ +Manage Employee Benefits +======================== +This module provides a comprehensive employee benefits management solution. +* Create benefits and their respective earnings and premiums +* Earnings and Premiums have effective dates to reflect changes over time +* The amounts in the benefit can be overriden in individual policies as necessary +* Benefits can be linked to payroll through the benefit code + +Some possible uses: +* Travel, Housing and other such allowances +* Employee personal medical expenses re-imbursement plans diff --git a/hr_benefit/readme/HISTORY.rst b/hr_benefit/readme/HISTORY.rst new file mode 100644 index 00000000..55d9b75b --- /dev/null +++ b/hr_benefit/readme/HISTORY.rst @@ -0,0 +1,4 @@ +14.0.1.0.1 (2022-04-07) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [FIX] Creating a policy no longer fails because of a non-existent benefit diff --git a/hr_benefit/security/ir.model.access.csv b/hr_benefit/security/ir.model.access.csv new file mode 100644 index 00000000..3a11c280 --- /dev/null +++ b/hr_benefit/security/ir.model.access.csv @@ -0,0 +1,15 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_hr_benefit_manager,access_hr_benefit,model_hr_benefit,hr.group_hr_manager,1,1,1,1 +access_hr_benefit_user,access_hr_benefit,model_hr_benefit,hr.group_hr_user,1,0,0,0 +access_hr_benefit_policy_mgr,access_hr_benefit_policy,model_hr_benefit_policy,hr.group_hr_manager,1,1,1,1 +access_hr_benefit_policy_user,access_hr_benefit_policy,model_hr_benefit_policy,hr.group_hr_user,1,1,1,0 +access_hr_benefit_policy_employee,access_hr_benefit_policy,model_hr_benefit_policy,base.group_user,1,0,0,0 +access_hr_benefit_premium_manager,access_hr_benefit_policy_premium,model_hr_benefit_premium,hr.group_hr_manager,1,1,1,1 +access_hr_benefit_premium_user,access_hr_benefit_policy_premium,model_hr_benefit_premium,hr.group_hr_user,1,0,0,0 +access_hr_benefit_claim_manager,access_hr_benefit_claim,model_hr_benefit_claim,hr.group_hr_manager,1,1,1,1 +access_hr_benefit_claim_user,access_hr_benefit_claim,model_hr_benefit_claim,hr.group_hr_user,1,0,0,0 +access_hr_benefit_adv_manager,access_hr_benefit_policy_adv,model_hr_benefit_advantage,hr.group_hr_manager,1,1,1,1 +access_hr_benefit_adv_user,access_hr_benefit_policy_adv,model_hr_benefit_advantage,hr.group_hr_user,1,0,0,0 +access_hr_benefit_policy_end,access_hr_benefit_policy_end,model_hr_benefit_policy_end,hr.group_hr_user,1,1,1,1 +access_hr_benefit_enroll_employee,access_hr_benefit_enroll_employee,model_hr_benefit_enroll_employee,hr.group_hr_user,1,1,1,1 +access_hr_benefit_enroll_multi_employee,access_hr_benefit_enroll_multi_employee,model_hr_benefit_enroll_multi_employee,hr.group_hr_user,1,1,1,1 diff --git a/hr_benefit/security/security.xml b/hr_benefit/security/security.xml new file mode 100644 index 00000000..bd28ad85 --- /dev/null +++ b/hr_benefit/security/security.xml @@ -0,0 +1,48 @@ + + + + + + Benefit Policy: Own and Subordinates + + + ['|',('employee_id.user_id', '=', user.id), + ('employee_id.department_id.manager_id.user_id', '=', user.id)] + + + + + + Benefit Policy: HR Officer can see all + + [(1, '=', 1)] + + + + + + + + + + Benefit: multi-company + + + + [('company_id', 'in', company_ids)] + + + + + + Benefit Policy: multi-company + + + + [('company_id', 'in', company_ids)] + + + + + + diff --git a/hr_benefit/static/description/icon.png b/hr_benefit/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/hr_benefit/static/description/icon.png differ diff --git a/hr_benefit/static/description/index.html b/hr_benefit/static/description/index.html new file mode 100644 index 00000000..ac1b7130 --- /dev/null +++ b/hr_benefit/static/description/index.html @@ -0,0 +1,425 @@ + + + + + + +Benefit Management + + + +
+

Benefit Management

+ + +

Beta License: AGPL-3 trevi-software/trevi-hr

+
+

Manage Employee Benefits

+

This module provides a comprehensive employee benefits management solution. +* Create benefits and their respective earnings and premiums +* Earnings and Premiums have effective dates to reflect changes over time +* The amounts in the benefit can be overriden in individual policies as necessary +* Benefits can be linked to payroll through the benefit code

+

Some possible uses: +* Travel, Housing and other such allowances +* Employee personal medical expenses re-imbursement plans

+

Table of contents

+
+
+

Changelog

+
+

14.0.1.0.1 (2022-04-07)

+
    +
  • [FIX] Creating a policy no longer fails because of a non-existent benefit
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • TREVI Software
  • +
  • Michael Telahun Makonnen
  • +
+
+
+

Other credits

+ +
+
+

Maintainers

+

This module is part of the trevi-software/trevi-hr project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/hr_benefit/static/src/img/main_screenshot.png b/hr_benefit/static/src/img/main_screenshot.png new file mode 100644 index 00000000..ab78e067 Binary files /dev/null and b/hr_benefit/static/src/img/main_screenshot.png differ diff --git a/hr_benefit/tests/__init__.py b/hr_benefit/tests/__init__.py new file mode 100644 index 00000000..b9322c47 --- /dev/null +++ b/hr_benefit/tests/__init__.py @@ -0,0 +1,7 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import common +from . import test_benefit_access +from . import test_benefit +from . import test_benefit_policy diff --git a/hr_benefit/tests/common.py b/hr_benefit/tests/common.py new file mode 100644 index 00000000..0867618e --- /dev/null +++ b/hr_benefit/tests/common.py @@ -0,0 +1,225 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from datetime import date + +from odoo.exceptions import AccessError +from odoo.tests import common, new_test_user + + +class TestBenefitCommon(common.SavepointCase): + @classmethod + def setUpClass(cls): + super(TestBenefitCommon, cls).setUpClass() + + cls.Benefit = cls.env["hr.benefit"] + cls.Premium = cls.env["hr.benefit.premium"] + cls.Earning = cls.env["hr.benefit.advantage"] + cls.Policy = cls.env["hr.benefit.policy"] + cls.Claim = cls.env["hr.benefit.claim"] + cls.EndWizard = cls.env["hr.benefit.policy.end"] + cls.EnrollWizard = cls.env["hr.benefit.enroll.employee"] + cls.EnrollMultiWizard = cls.env["hr.benefit.enroll.multi.employee"] + + # Company A + cls.company1 = cls.env["res.company"].create({"name": "A company"}) + + # Normal User John + cls.userJohn = new_test_user( + cls.env, + login="john", + groups="base.group_user", + name="John", + company_ids=[(6, 0, cls.env.companies.ids)], + ) + cls.eeJohn = cls.env["hr.employee"].create( + {"name": "John", "user_id": cls.userJohn.id} + ) + + # Normal User Paul + cls.userPaul = new_test_user( + cls.env, + login="paul", + groups="base.group_user", + name="Paul", + company_ids=[(6, 0, cls.env.companies.ids)], + ) + cls.eePaul = cls.env["hr.employee"].create( + {"name": "Paul", "user_id": cls.userPaul.id} + ) + + # Company A User James + cls.userJames = new_test_user( + cls.env, + login="james", + groups="base.group_user", + name="James", + company_id=cls.company1.id, + company_ids=[(6, 0, cls.company1.ids)], + ) + cls.eeJames = ( + cls.env["hr.employee"] + .with_context(allowed_company_ids=cls.company1.ids) + .create({"name": "James", "user_id": cls.userJames.id}) + ) + + cls.benefit_create_vals = {"name": "A", "code": "A"} + + # HR Manager + cls.userHRM = new_test_user( + cls.env, + login="hrm", + groups="base.group_user,hr.group_hr_manager", + name="Payroll manager", + email="hrm@example.com", + company_ids=[(6, 0, cls.env.companies.ids)], + ) + # HR User + cls.userHRO = new_test_user( + cls.env, + login="hro", + groups="base.group_user,hr.group_hr_user", + name="Payroll officer", + email="hro@example.com", + company_ids=[(6, 0, cls.env.companies.ids)], + ) + + def create_policy( + self, + employee, + benefit, + start=False, + end=False, + advantage=False, + premium=False, + premium_total=False, + ): + _dict = { + "employee_id": employee.id, + "benefit_id": benefit.id, + "start_date": start is False and date.today() or start, + "end_date": end, + } + if advantage is not False: + _dict["advantage_override"] = True + _dict["advantage_amount"] = advantage + if premium is not False: + _dict["premium_override"] = True + _dict["premium_override_amount"] = premium + _dict["premium_override_total"] = premium_total + return self.Policy.create(_dict) + + def create_premium( + self, benefit, start=False, ptype="monthly", amount=0, total=False + ): + _dict = { + "benefit_id": benefit.id, + "effective_date": start is False and date.today() or start, + "type": ptype, + "amount": amount, + } + if total is not False: + _dict["total_amount"] = total + benefit.has_premium = True + return self.Premium.create(_dict) + + def create_earning( + self, + benefit, + start=False, + ptype="allowance", + limit=0, + limit_period="monthly", + limit_mo_day="1", + allowance=0, + loan=0, + mindays=0, + ): + _dict = { + "benefit_id": benefit.id, + "effective_date": start is False and date.today() or start, + "type": ptype, + "allowance_amount": allowance, + "loan_amount": loan, + "min_employed_days": mindays, + } + if ptype == "reimburse": + _dict["reim_limit_amount"] = limit + _dict["reim_limit_period"] = limit_period + _dict["reim_period_month_day"] = limit_mo_day + benefit.has_advantage = True + return self.Earning.create(_dict) + + def create_claim(self, policy, amount, dt=None): + if dt is None: + dt = date.today() + return self.Claim.create( + { + "date": dt, + "benefit_policy_id": policy.id, + "employee_id": policy.employee_id.id, + "amount_requested": amount, + } + ) + + def create_benefit(self, vals): + + return self.Benefit.create(vals) + + def create_contract(self, state, kanban_state, start, end=None, trial_end=None): + return self.env["hr.contract"].create( + { + "name": "Contract", + "employee_id": self.eeJohn.id, + "state": state, + "kanban_state": kanban_state, + "wage": 1, + "date_start": start, + "trial_date_end": trial_end, + "date_end": end, + } + ) + + def create_fails(self, user, obj, vals): + with self.assertRaises(AccessError): + obj.with_user(user).create(vals) + + def create_succeeds(self, user, obj, vals): + res = None + try: + res = obj.with_user(user).create(vals) + except AccessError: + self.fail("Caught unexpected exception creating {}".format(obj._name)) + return res + + def unlink_fails(self, user, obj): + with self.assertRaises(AccessError): + obj.with_user(user).unlink() + + def unlink_succeeds(self, user, obj): + try: + obj.with_user(user).unlink() + except AccessError: + self.fail("Caught unexpected exception unlinking {}".format(obj._name)) + + def read_succeeds(self, user, obj, obj_id): + try: + obj.with_user(user).browse(obj_id).read([]) + except AccessError: + self.fail("Caught an unexpected exception reading {}".format(obj._name)) + + def read_fails(self, user, obj, obj_id): + with self.assertRaises(AccessError): + obj.with_user(user).browse(obj_id).read([]) + + # Pre-requisite: READ Access + def write_fails(self, user, obj, obj_id, write_vals): + with self.assertRaises(AccessError): + obj.with_user(user).browse(obj_id).write(write_vals) + + # Pre-requisite: READ Access + def write_succeeds(self, user, obj, obj_id, write_vals): + try: + obj.with_user(user).browse(obj_id).write(write_vals) + except AccessError: + self.fail("Caught an unexpected exception writing {}".format(obj._name)) diff --git a/hr_benefit/tests/test_benefit.py b/hr_benefit/tests/test_benefit.py new file mode 100644 index 00000000..3ba34e4d --- /dev/null +++ b/hr_benefit/tests/test_benefit.py @@ -0,0 +1,187 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from datetime import date + +from dateutil.relativedelta import relativedelta + +from odoo import fields +from odoo.exceptions import UserError + +from . import common + + +class TestBenefit(common.TestBenefitCommon): + def test_get_advantage_no_benefit(self): + """#29 Try getting an advantage from an empty benefit recordset""" + + self.Benefit.get_latest_advantage(date.today()) + + def test_get_premium_no_benefit(self): + """#29 Try getting a premium from an empty benefit recordset""" + + self.Benefit.get_latest_premium(date.today()) + + def test_get_latest_earning(self): + """Get the earning with the latest effective date that is not in the future""" + + bn = self.create_benefit(self.benefit_create_vals) + self.create_earning(bn, date.today() - relativedelta(days=1)) + earnToday = self.create_earning(bn, date.today()) + self.create_earning(bn, date.today() + relativedelta(days=1)) + latest = bn.get_latest_advantage(date.today()) + + self.assertEqual(3, len(bn.advantage_ids)) + self.assertEqual(earnToday, latest) + + def test_get_latest_premium(self): + """Get the premium with the latest effective date that is not in the future""" + + bn = self.create_benefit(self.benefit_create_vals) + self.create_premium(bn, date.today() - relativedelta(days=1)) + prmToday = self.create_premium(bn, date.today()) + self.create_premium(bn, date.today() + relativedelta(days=1)) + latest = bn.get_latest_premium(date.today()) + + self.assertEqual(3, len(bn.premium_ids)) + self.assertEqual(prmToday, latest) + + def test_monthly_premium_no_installments(self): + """If amount and total are equal only 1 installment""" + + bn = self.create_benefit(self.benefit_create_vals) + prm = self.create_premium( + bn, date.today() - relativedelta(days=1), "monthly", 100, 100 + ) + + self.assertEqual(1, prm.no_of_installments) + self.assertEqual(100, prm.amount) + self.assertEqual(100, prm.total_amount) + + def test_monthly_premium_installments(self): + """If total is a multiple of amount then installment = total/amount""" + + bn = self.create_benefit(self.benefit_create_vals) + prm = self.create_premium( + bn, date.today() - relativedelta(days=1), "monthly", 100, 300 + ) + + self.assertEqual(3, prm.no_of_installments) + self.assertEqual(100, prm.amount) + self.assertEqual(300, prm.total_amount) + + def test_monthly_premium_installments_plus(self): + """ + If total is not an even multiple of amount then + installment = (total/amount) + 1 + """ + + bn = self.create_benefit(self.benefit_create_vals) + prm = self.create_premium( + bn, date.today() - relativedelta(days=1), "monthly", 100, 350 + ) + + self.assertEqual(4, prm.no_of_installments) + self.assertEqual(100, prm.amount) + self.assertEqual(350, prm.total_amount) + + def test_reimburse_remaining_no_policy(self): + """ + If the employee does not have a re-imbursement policy then the + amount to be re-imbursed is zero. + """ + + bn = self.create_benefit(self.benefit_create_vals) + earn = self.create_earning(bn, date.today(), "reimburse", 1000) + + self.assertTrue( + fields.Float.is_zero( + earn.get_reimburse_remaining(self.eeJohn, date.today()), + precision_digits=2, + ) + ) + + def test_reimburse_remaining_no_claims(self): + """ + If no claims have been made in the period the full amount + remains to be re-imbursed + """ + + bn = self.create_benefit(self.benefit_create_vals) + self.create_policy(self.eeJohn, bn, date.today()) + earn = self.create_earning(bn, date.today(), "reimburse", 1000) + + self.assertEqual( + 0, + fields.Float.compare( + earn.get_reimburse_remaining(self.eeJohn, date.today()), + 1000, + precision_digits=2, + ), + ) + + def test_reimburse_remaining_with_claims(self): + """ + If claims have been made in the period for the full amount then + nothing remains to be re-imbursed + """ + + bn = self.create_benefit(self.benefit_create_vals) + earn = self.create_earning(bn, date.today(), "reimburse", 1000) + pol = self.create_policy(self.eeJohn, bn, date.today()) + clm = self.create_claim(pol, 1000) + clm.claim_approve() + + self.assertTrue( + fields.Float.is_zero( + earn.get_reimburse_remaining(self.eeJohn, date.today()), + precision_digits=2, + ) + ) + + def test_delete_claim(self): + """A claim may not be deleted unless it's in a 'draft' state""" + + bn = self.create_benefit(self.benefit_create_vals) + self.create_earning(bn, date.today(), "reimburse", 1000) + pol = self.create_policy(self.eeJohn, bn, date.today()) + clm = self.create_claim(pol, 500) + clm2 = self.create_claim(pol, 500) + clm2.claim_approve() + + try: + clm.unlink() + except UserError: + self.fail("Unexpected exception") + + with self.assertRaises(UserError): + clm2.unlink() + + def test_set_draft_from_approve(self): + """Setting state to 'draft' when it is 'approv' raises an error""" + + bn = self.create_benefit(self.benefit_create_vals) + pol = self.create_policy(self.eeJohn, bn, date.today()) + clm = self.create_claim(pol, 1000) + clm.claim_approve() + with self.assertRaises(UserError): + clm.set_to_draft() + + def test_multicompany_nosearch(self): + """A benefit in one company does not appear in searches by another""" + + bn = self.create_benefit(self.benefit_create_vals) + self.assertNotEqual( + bn.company_id, + self.company1, + "Company of benefit is not equal to 'A Company'", + ) + + lst = ( + self.Benefit.with_user(self.userHRO) + .with_context(allowed_company_ids=self.company1.ids) + .search([("code", "=", "A")]) + ) + self.assertEqual( + len(lst), 0, "Benefit does not appear in searches by 'A Company'" + ) diff --git a/hr_benefit/tests/test_benefit_access.py b/hr_benefit/tests/test_benefit_access.py new file mode 100644 index 00000000..2802cd89 --- /dev/null +++ b/hr_benefit/tests/test_benefit_access.py @@ -0,0 +1,136 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from datetime import date + +from dateutil.relativedelta import relativedelta + +from . import common + + +class TestBenefitAccess(common.TestBenefitCommon): + def test_benefit_access(self): + """hr.benefit access: HRM - full access, HRO read-only""" + + bn = self.create_benefit(self.benefit_create_vals) + + # HRM + self.create_succeeds(self.userHRM, self.Benefit, self.benefit_create_vals) + self.read_succeeds(self.userHRM, self.Benefit, bn.id) + self.write_succeeds(self.userHRM, self.Benefit, bn.id, {"name": "tba"}) + self.unlink_succeeds(self.userHRM, bn) + + # HRO + self.create_fails(self.userHRO, self.Benefit, self.benefit_create_vals) + self.unlink_fails(self.userHRO, bn) + self.read_succeeds(self.userHRO, self.Benefit, bn.id) + self.write_fails(self.userHRO, self.Benefit, bn.id, {"name": "tba"}) + + def test_premium_access(self): + """hr.benefit.premium access: HRM - full access, HRO read-only""" + + bn = self.create_benefit(self.benefit_create_vals) + create_vals = { + "benefit_id": bn.id, + "effective_date": date.today(), + "type": "monthly", + } + prm = self.create_premium(bn, date.today() + relativedelta(days=30)) + + # HRM + self.create_succeeds(self.userHRM, self.Premium, create_vals) + self.unlink_succeeds(self.userHRM, prm) + self.read_succeeds(self.userHRM, self.Premium, prm.id) + self.write_succeeds(self.userHRM, self.Premium, prm.id, {"type": "annual"}) + + # HRO + self.create_fails(self.userHRO, self.Premium, create_vals) + self.unlink_fails(self.userHRO, prm) + self.read_succeeds(self.userHRO, self.Premium, prm.id) + self.write_fails(self.userHRO, self.Premium, prm.id, {"type": "annual"}) + + def test_earning_access(self): + """hr.benefit.advantage access: HRM - full access, HRO read-only""" + + bn = self.create_benefit(self.benefit_create_vals) + create_vals = { + "benefit_id": bn.id, + "effective_date": date.today(), + "type": "allowance", + } + earn = self.create_earning(bn, date.today() + relativedelta(days=30)) + + # HRM + self.create_succeeds(self.userHRM, self.Earning, create_vals) + self.unlink_succeeds(self.userHRM, earn) + self.read_succeeds(self.userHRM, self.Earning, earn.id) + self.write_succeeds(self.userHRM, self.Earning, earn.id, {"type": "reimburse"}) + + # HRO + self.create_fails(self.userHRO, self.Earning, create_vals) + self.unlink_fails(self.userHRO, earn) + self.read_succeeds(self.userHRO, self.Earning, earn.id) + self.write_fails(self.userHRO, self.Earning, earn.id, {"type": "reimburse"}) + + def test_policy_access(self): + """hr.benefit.policy access: HRM - full access, HRO read-only""" + + bn1 = self.create_benefit(self.benefit_create_vals) + bn2 = self.create_benefit( + { + "name": "BenefitB", + "code": "B", + "multi_policy": True, + } + ) + pol = self.create_policy(self.eeJohn, bn1, date.today()) + + # HRM + hroPol = self.create_succeeds( + self.userHRM, + self.Policy, + { + "name": "tbp", + "employee_id": self.eeJohn.id, + "benefit_id": bn2.id, + "start_date": date.today(), + }, + ) + self.unlink_succeeds(self.userHRM, hroPol) + self.read_succeeds(self.userHRM, self.Policy, pol.id) + self.write_succeeds(self.userHRM, self.Policy, pol.id, {"name": "tbp2"}) + + # HRO + hroPol = self.create_succeeds( + self.userHRO, + self.Policy, + { + "name": "tbp", + "employee_id": self.eeJohn.id, + "benefit_id": bn2.id, + "start_date": date.today(), + }, + ) + self.unlink_fails(self.userHRO, hroPol) + self.read_succeeds(self.userHRO, self.Policy, pol.id) + self.write_succeeds(self.userHRO, self.Policy, pol.id, {"name": "tbp2"}) + + def test_policy_user_own(self): + """A user can only read his/her own policies""" + + bn1 = self.create_benefit(self.benefit_create_vals) + polJohn = self.create_policy(self.eeJohn, bn1, date.today()) + polPaul = self.create_policy(self.eePaul, bn1, date.today()) + grpOfficer = self.env.ref("hr.group_hr_user") + self.assertNotIn(grpOfficer, self.userJohn.groups_id) + self.assertNotEqual(polJohn, polPaul) + self.assertEqual(self.userJohn, polJohn.employee_id.user_id) + self.assertEqual(self.userPaul, polPaul.employee_id.user_id) + + # John can read his own policy + self.read_succeeds(self.userJohn, self.Policy, polJohn.id) + # but not Paul's + self.read_fails(self.userJohn, self.Policy, polPaul.id) + # John can't modify his own policy or Paul's + self.write_fails(self.userJohn, self.Policy, polJohn.id, {"note": "A"}) + self.write_fails(self.userJohn, self.Policy, polPaul.id, {"note": "A"}) diff --git a/hr_benefit/tests/test_benefit_policy.py b/hr_benefit/tests/test_benefit_policy.py new file mode 100644 index 00000000..c15ff7cf --- /dev/null +++ b/hr_benefit/tests/test_benefit_policy.py @@ -0,0 +1,287 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from datetime import date + +from dateutil.relativedelta import relativedelta + +from odoo import fields +from odoo.exceptions import UserError + +from . import common + + +class TestBenefit(common.TestBenefitCommon): + def test_policy_onchange_benefit(self): + """When the benefit changes so should the code, premium and earnings amounts""" + + bn1 = self.create_benefit(self.benefit_create_vals) + self.create_premium( + bn1, date.today() - relativedelta(days=1), "monthly", 100, 100 + ) + bn2 = self.create_benefit({"name": "B", "code": "B"}) + self.create_earning(bn2, date.today(), allowance=1000) + pol = self.create_policy(self.eeJohn, bn1, date.today()) + self.assertEqual(bn1, pol.benefit_id) + self.assertEqual(bn1.code, pol.benefit_code) + self.assertEqual("draft", pol.state) + self.assertTrue(fields.Float.is_zero(pol.advantage_amount, precision_digits=2)) + self.assertEqual(100, pol.premium_amount) + self.assertEqual(100, pol.premium_total) + + pol.benefit_id = bn2.id + + self.assertEqual(bn2, pol.benefit_id) + self.assertEqual(bn2.code, pol.benefit_code) + self.assertEqual("draft", pol.state) + self.assertEqual( + 0, fields.Float.compare(1000, pol.advantage_amount, precision_digits=2) + ) + self.assertTrue(fields.Float.is_zero(pol.premium_amount, precision_digits=2)) + self.assertTrue(fields.Float.is_zero(pol.premium_total, precision_digits=2)) + + def test_policy_premium_latest(self): + """ + If there are multiple premiums get the lastest one as of today, even if + it was after the policy creation date. + """ + + bn = self.create_benefit(self.benefit_create_vals) + self.create_premium( + bn, + date.today() - relativedelta(days=60), + ptype="monthly", + amount=100, + ) + self.create_premium( + bn, + date.today() - relativedelta(days=30), + ptype="monthly", + amount=200, + ) + pol = self.create_policy(self.eeJohn, bn, date.today() - relativedelta(days=45)) + + self.assertEqual(200, pol.premium_amount) + + def test_policy_premium_installments(self): + """If total is a multiple of amount then installment = total/amount""" + + bn = self.create_benefit(self.benefit_create_vals) + self.create_premium( + bn, + date.today() - relativedelta(days=1), + ptype="monthly", + amount=100, + total=300, + ) + pol = self.create_policy(self.eeJohn, bn, date.today()) + + self.assertEqual(100, pol.premium_amount) + self.assertEqual(300, pol.premium_total) + self.assertEqual(3, pol.premium_installments) + + def test_monthly_premium_installments_plus(self): + """ + If total is not an even multiple of amount then + installment = (total/amount) + 1 + """ + + bn = self.create_benefit(self.benefit_create_vals) + self.create_premium( + bn, date.today() - relativedelta(days=1), "monthly", 100, 350 + ) + pol = self.create_policy(self.eeJohn, bn, date.today()) + + self.assertEqual(100, pol.premium_amount) + self.assertEqual(350, pol.premium_total) + self.assertEqual(4, pol.premium_installments) + + def test_min_employed_days(self): + """ + If the employee has been employed less than the min. days specified in + the benefit a UserError exception is raised + """ + + cc = self.create_contract( + "draft", "done", date.today() - relativedelta(days=10) + ) + cc.signal_confirm() + bn = self.create_benefit({"name": "B", "code": "B", "min_employed_days": 30}) + self.create_earning(bn, date.today(), allowance=1000) + + with self.assertRaises(UserError): + self.create_policy(self.eeJohn, bn, date.today()) + + def test_no_multipolicy(self): + """ + If the benefit hasn't enabled concurrent policies an attempt to enroll an + employee more than once will cause a UserError exception to be raised + """ + + bnNoMulti = self.create_benefit({"name": "B", "code": "B"}) + bnMulti = self.create_benefit({"name": "C", "code": "C", "multi_policy": True}) + self.create_earning(bnNoMulti, date.today(), allowance=1000) + self.create_earning(bnMulti, date.today(), allowance=3000) + self.create_policy(self.eeJohn, bnNoMulti, date.today()) + self.create_policy(self.eeJohn, bnMulti, date.today()) + + with self.assertRaises(UserError): + self.create_policy(self.eeJohn, bnNoMulti, date.today()) + + try: + self.create_policy(self.eeJohn, bnMulti, date.today()) + except UserError: + self.fail("An unexpected exception was raised") + + def test_policy_unlink(self): + """Deleting a policy not in 'Draft' state raises a UserError""" + + bn = self.create_benefit({"name": "B", "code": "B"}) + self.create_earning(bn, date.today(), allowance=1000) + pol = self.create_policy(self.eeJohn, bn, date.today()) + pol.state_open() + + with self.assertRaises(UserError): + pol.unlink() + + def test_set_draft_from_open(self): + """Setting state to 'Draft' when it is 'open' raises an error""" + + bn = self.create_benefit({"name": "B", "code": "B"}) + self.create_earning(bn, date.today(), allowance=1000) + pol = self.create_policy(self.eeJohn, bn, date.today()) + pol.state_open() + + with self.assertRaises(UserError): + pol.state = "draft" + + def test_set_done_from_draft(self): + """Setting state to 'Done' when it isn't 'open' raises an error""" + + bn = self.create_benefit({"name": "B", "code": "B"}) + self.create_earning(bn, date.today(), allowance=1000) + pol = self.create_policy(self.eeJohn, bn, date.today()) + + with self.assertRaises(UserError): + pol.state_done() + + def test_change_from_done(self): + """Setting state to any value when it is 'done' raises an error""" + + bn = self.create_benefit({"name": "B", "code": "B"}) + self.create_earning(bn, date.today(), allowance=1000) + pol = self.create_policy(self.eeJohn, bn, date.today()) + pol.state_open() + pol.state_done() + + with self.assertRaises(UserError): + pol.state = "draft" + with self.assertRaises(UserError): + pol.state = "open" + + def test_override_earning(self): + """User can override the calculated earning amount""" + + bn = self.create_benefit({"name": "B", "code": "B"}) + self.create_earning(bn, date.today(), allowance=1000) + pol = self.create_policy(self.eeJohn, bn, date.today(), advantage=4400) + pol.state_open() + + self.assertEqual(4400, pol.advantage_amount) + + def test_end_policy_wizard(self): + """Calling end_policy() method of wizard sets end date and state = 'done'""" + + bn = self.create_benefit({"name": "B", "code": "B"}) + self.create_earning(bn, date.today(), allowance=1000) + pol = self.create_policy(self.eeJohn, bn, date.today()) + pol.state_open() + self.assertFalse(pol.end_date) + self.assertEqual("open", pol.state) + + wiz = self.EndWizard.with_context({"end_benefit_policy_id": pol.id}).create({}) + wiz.date = date.today() + wiz.end_policy() + + self.assertEqual(date.today(), pol.end_date) + self.assertEqual(pol.state, "done") + + def test_endroll_single_employee(self): + """Running the enroll employee wizard creates a policy for the employee""" + + bn = self.create_benefit({"name": "B", "code": "B"}) + self.create_earning(bn, date.today(), allowance=1000) + + wiz = self.EnrollWizard.with_context({"active_id": bn.id}).create( + {"employee_id": self.eeJohn.id, "start_date": date.today()} + ) + wiz.do_enroll() + + policy_ids = self.Policy.search([("employee_id", "=", self.eeJohn.id)]) + self.assertEqual(1, len(policy_ids)) + self.assertEqual(bn, policy_ids[0].benefit_id) + self.assertEqual(date.today(), policy_ids[0].start_date) + self.assertFalse(policy_ids[0].end_date) + + def test_endroll_multi_employee(self): + """ + Running the enroll multiple employees wizard creates a policy for + multiple employees + """ + + bn = self.create_benefit({"name": "B", "code": "B"}) + self.create_earning(bn, date.today(), allowance=1000) + + wiz = self.EnrollMultiWizard.with_context({"active_id": bn.id}).create( + {"employee_ids": [(6, 0, [self.eeJohn.id])], "start_date": date.today()} + ) + wiz.do_multi_enroll() + + policy_ids = self.Policy.search([("employee_id", "=", self.eeJohn.id)]) + self.assertEqual(1, len(policy_ids)) + self.assertEqual(bn, policy_ids[0].benefit_id) + self.assertEqual(date.today(), policy_ids[0].start_date) + self.assertFalse(policy_ids[0].end_date) + + def test_multicompany_nosearch(self): + """A policy in one company does not appear in searches by another""" + + bn = self.create_benefit(self.benefit_create_vals) + pol = self.create_policy(self.eeJohn, bn, date.today()) + self.assertNotEqual( + pol.company_id, + self.company1, + "Company of policy is not equal to 'A Company'", + ) + + lst = ( + self.Policy.with_user(self.userHRO) + .with_context(allowed_company_ids=self.company1.ids) + .search([("name", "=", pol.name)]) + ) + self.assertEqual( + len(lst), 0, "Policy does not appear in searches by 'A Company'" + ) + + def test_multicompany_employee(self): + """Creating policy for employee from different company raises exception""" + + self.assertEqual( + self.eeJames.company_id, self.company1, "Company of employee is 'A Company'" + ) + + bn = self.create_benefit(self.benefit_create_vals) + self.assertNotEqual( + bn.company_id, + self.company1, + "Company of benefit is not equal to 'A Company'", + ) + + with self.assertRaises(UserError): + pol = self.create_policy(self.eeJames, bn, date.today()) + + self.assertNotEqual( + pol.company_id, + self.company1, + "Company of policy is not equal to 'A Company'", + ) diff --git a/hr_benefit/views/benefit_claim_view.xml b/hr_benefit/views/benefit_claim_view.xml new file mode 100644 index 00000000..d4459c02 --- /dev/null +++ b/hr_benefit/views/benefit_claim_view.xml @@ -0,0 +1,135 @@ + + + + + + + + hr.benefit.claim.filter + hr.benefit.claim + + + + + + + + + + + + + + + + + + + + + + hr.benefit.claim.tree + hr.benefit.claim + + + + + + + + + + + + + + hr.benefit.claim.form + hr.benefit.claim + + +
+
+
+ + + + + + + + + + +
+
+
+ + + All Claims + hr.benefit.claim + tree,form + + + + +
+
diff --git a/hr_benefit/views/benefit_earning_view.xml b/hr_benefit/views/benefit_earning_view.xml new file mode 100644 index 00000000..936a85a8 --- /dev/null +++ b/hr_benefit/views/benefit_earning_view.xml @@ -0,0 +1,132 @@ + + + + + + + + hr.benefit.advantage.tree + hr.benefit.advantage + + + + + + + + + + + + + hr.benefit.advantage.form + hr.benefit.advantage + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + Earnings + hr.benefit.advantage + tree,form + + + +
+
diff --git a/hr_benefit/views/benefit_policy_view.xml b/hr_benefit/views/benefit_policy_view.xml new file mode 100644 index 00000000..e0983486 --- /dev/null +++ b/hr_benefit/views/benefit_policy_view.xml @@ -0,0 +1,204 @@ + + + + + + + + hr.benefit.policy.filter + hr.benefit.policy + + + + + + + + + + + + + + + + + + + + + hr.benefit.policy.tree + hr.benefit.policy + + + + + + + + + + + + + + + + hr.benefit.policy.form + hr.benefit.policy + +
+
+
+ + +
+
+
+ + + Policies + hr.benefit.policy + tree,form + {'search_default_is_active': 1} + + + + + Policies + hr.benefit.policy + tree,form + {'search_default_my_policies':1, 'search_default_is_active':1} + + + + +
+
diff --git a/hr_benefit/views/benefit_premium_view.xml b/hr_benefit/views/benefit_premium_view.xml new file mode 100644 index 00000000..02b0d7a7 --- /dev/null +++ b/hr_benefit/views/benefit_premium_view.xml @@ -0,0 +1,61 @@ + + + + + + + + hr.benefit.premium.tree + hr.benefit.premium + + + + + + + + + + + + + + + hr.benefit.premium.form + hr.benefit.premium + +
+ + + + + + + + + + + + + + + +
+
+
+ + + Premiums + hr.benefit.premium + tree,form + + + +
+
diff --git a/hr_benefit/views/benefit_view.xml b/hr_benefit/views/benefit_view.xml new file mode 100644 index 00000000..adeefdfd --- /dev/null +++ b/hr_benefit/views/benefit_view.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + hr.benefit.tree + hr.benefit + + + + + + + + + + + + + + + hr.benefit.form + hr.benefit + +
+ + +
+
+
+ + + Benefits + hr.benefit + tree,form + + + +
+
diff --git a/hr_benefit/views/hr_employee_view.xml b/hr_benefit/views/hr_employee_view.xml new file mode 100644 index 00000000..1cbdda00 --- /dev/null +++ b/hr_benefit/views/hr_employee_view.xml @@ -0,0 +1,63 @@ + + + + + + + + hr.benefit.policy + Benefits + tree,form + {'search_default_employee_id': [active_id], 'default_employee_id': active_id, 'search_default_group_employee_id': 0} + + + hr.benefit.claim + Claims on Benefits + tree,form + {'search_default_employee_id': [active_id], 'default_employee_id': active_id} + + + hr.employee.view.inherit.benefits + hr.employee + + + + + + + + + + + + + diff --git a/hr_benefit/wizard/__init__.py b/hr_benefit/wizard/__init__.py new file mode 100644 index 00000000..d4822a80 --- /dev/null +++ b/hr_benefit/wizard/__init__.py @@ -0,0 +1,7 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013,2014 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import end_policy +from . import enroll_employee +from . import enroll_multi_employee diff --git a/hr_benefit/wizard/end_policy.py b/hr_benefit/wizard/end_policy.py new file mode 100644 index 00000000..c0e1d589 --- /dev/null +++ b/hr_benefit/wizard/end_policy.py @@ -0,0 +1,30 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2014 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class PolicyEnd(models.TransientModel): + + _name = "hr.benefit.policy.end" + _description = "Benefit Policy Termination Wizard" + + date = fields.Date(string="End Date", required=True, default=fields.Date.today()) + + @api.model + def _get_policy(self): + + return self.env.context.get("end_benefit_policy_id", False) + + def end_policy(self): + + policy_id = self._get_policy() + if not policy_id: + raise ValidationError(_("Unable to determine Benefit Policy.")) + + policy = self.env["hr.benefit.policy"].browse(policy_id) + policy.end_date = self.date + policy.state_done() + return {"type": "ir.actions.act_window_close"} diff --git a/hr_benefit/wizard/end_policy.xml b/hr_benefit/wizard/end_policy.xml new file mode 100644 index 00000000..a92be317 --- /dev/null +++ b/hr_benefit/wizard/end_policy.xml @@ -0,0 +1,34 @@ + + + + + + hr.benefit.policy.end.form + hr.benefit.policy.end + +
+
+
+ +

+ Please enter the last day of the policy in the field provided below. +

+ + + + + + + + +
+
+ +
+
diff --git a/hr_benefit/wizard/enroll_employee.py b/hr_benefit/wizard/enroll_employee.py new file mode 100644 index 00000000..bfae7235 --- /dev/null +++ b/hr_benefit/wizard/enroll_employee.py @@ -0,0 +1,47 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +class EnrollEmployee(models.TransientModel): + + _name = "hr.benefit.enroll.employee" + _description = "Employee Benefit Enrollment Form" + + benefit_id = fields.Many2one( + string="Benefit", + comodel_name="hr.benefit", + required=True, + default=lambda self: self._get_benefit(), + ) + employee_id = fields.Many2one( + comodel_name="hr.employee", string="Employee", required=True + ) + start_date = fields.Date( + string="Enrollment Date", required=True, default=fields.Date.today() + ) + end_date = fields.Date(string="Termination Date") + + @api.model + def _get_benefit(self): + + if self.env.context is None: + self.env.with_context({}) + return self.env.context.get("active_id", False) + + def do_enroll(self): + + if not self.benefit_id or not self.employee_id: + return {"type": "ir.actions.act_window_close"} + + vals = { + "benefit_id": self.benefit_id.id, + "employee_id": self.employee_id.id, + "start_date": self.start_date, + "end_date": self.end_date, + } + self.env["hr.benefit.policy"].create(vals) + + return {"type": "ir.actions.act_window_close"} diff --git a/hr_benefit/wizard/enroll_employee_view.xml b/hr_benefit/wizard/enroll_employee_view.xml new file mode 100644 index 00000000..f3d4b8f6 --- /dev/null +++ b/hr_benefit/wizard/enroll_employee_view.xml @@ -0,0 +1,50 @@ + + + + + + hr.benefit.enroll.employee.form + hr.benefit.enroll.employee + +
+
+
+ + + + + + + + +
+
+
+ + Enroll Employee + hr.benefit.enroll.employee + form + + new + + + Enroll Employee + + + form + code + +action_values = env.ref("hr_benefit.action_enroll_employee").sudo().read()[0] +action_values.update({"context": env.context}) +action = action_values + + + +
+
diff --git a/hr_benefit/wizard/enroll_multi_employee.py b/hr_benefit/wizard/enroll_multi_employee.py new file mode 100644 index 00000000..f6a19318 --- /dev/null +++ b/hr_benefit/wizard/enroll_multi_employee.py @@ -0,0 +1,116 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import math +from datetime import date + +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models + + +class EnrollEmployee(models.TransientModel): + + _name = "hr.benefit.enroll.multi.employee" + _description = "Employee Benefit Enrollment Form" + + benefit_id = fields.Many2one( + string="Benefit", + comodel_name="hr.benefit", + required=True, + default=lambda self: self._get_benefit(), + ) + employee_ids = fields.Many2many( + string="Employee", + comodel_name="hr.employee", + relation="hr_employee_benefit_rel", + column1="employee_id", + column2="benefit_id", + ) + start_date = fields.Date( + string="Enrollment Date", required=True, default=date.today() + ) + end_date = fields.Date(string="Termination Date") + advantage_override = fields.Boolean(string="Override Advantage") + premium_override = fields.Boolean(string="Override Premium") + advantage_amount = fields.Float(compute="_compute_amounts", digits="Account") + premium_amount = fields.Float(compute="_compute_amounts", digits="Account") + premium_total = fields.Float(compute="_compute_amounts", digits="Account") + premium_installments = fields.Integer( + compute="_compute_premium_installments", string="No. of Installments" + ) + + @api.model + def _get_benefit(self): + + if self.env.context is None: + self.env.context = {} + return self.env.context.get("active_id", False) + + @api.depends("benefit_id") + def _compute_amounts(self): + + for rec in self: + res = { + "advantage_amount": 0, + "premium_amount": 0, + "premium_total": 0, + } + + if not rec.benefit_id or not rec.start_date: + rec.write(res) + + dToday = rec.start_date + adv = rec.benefit_id.get_latest_advantage(dToday) + prm = rec.benefit_id.get_latest_premium(dToday) + if adv is not None: + if adv.type == "allowance": + res["advantage_amount"] = adv.allowance_amount + + if prm is not None: + res["premium_amount"] = prm.amount + res["premium_total"] = prm.total_amount + + rec.write(res) + + @api.depends("start_date", "premium_amount", "premium_total") + def _compute_premium_installments(self): + + for rec in self: + res = {"end_date": False, "premium_installments": 0} + if rec.premium_amount == 0: + rec.write(res) + return + + installments = int( + math.ceil(float(rec.premium_total) / float(rec.premium_amount)) + ) + if installments > 0: + dEnd = rec.start_date + relativedelta(months=+installments) + res["end_date"] = dEnd + res["premium_installments"] = installments + rec.write(res) + + def do_multi_enroll(self): + + if not self.benefit_id or len(self.employee_ids) == 0: + return {"type": "ir.actions.act_window_close"} + + for employee in self.employee_ids: + + vals = { + "benefit_id": self.benefit_id.id, + "employee_id": employee.id, + "start_date": self.start_date, + "end_date": self.end_date, + "advantage_override": self.advantage_override, + "premium_override": self.premium_override, + "advantage_amount": self.advantage_amount, + "premium_amount": self.premium_amount, + "premium_total": self.premium_total, + } + pol_id = self.env["hr.benefit.policy"].create(vals) + pol_id.state_open() + + return {"type": "ir.actions.act_window_close"} diff --git a/hr_benefit/wizard/enroll_multi_employee_view.xml b/hr_benefit/wizard/enroll_multi_employee_view.xml new file mode 100644 index 00000000..12d2aea5 --- /dev/null +++ b/hr_benefit/wizard/enroll_multi_employee_view.xml @@ -0,0 +1,74 @@ + + + + + + hr.benefit.enroll.employee.multi.form + hr.benefit.enroll.multi.employee + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + Enroll Multiple Employees + hr.benefit.enroll.multi.employee + form + + new + + + Enroll Multiple Employees + + + form + code + +action_values = env.ref("hr_benefit.action_enroll_multiemployee").sudo().read()[0] +action_values.update({"context": env.context}) +action = action_values + + + +
+
diff --git a/hr_benefit_payroll/README.rst b/hr_benefit_payroll/README.rst new file mode 100644 index 00000000..70331637 --- /dev/null +++ b/hr_benefit_payroll/README.rst @@ -0,0 +1,86 @@ +================ +Benefits Payroll +================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:284204823bfbe256db00a02ca5635fb5ce22d4c9852bcb61cd9e2491875d03d7 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-trevi--software%2Ftrevi--hr-lightgray.png?logo=github + :target: https://github.com/trevi-software/trevi-hr/tree/14.0/hr_benefit_payroll + :alt: trevi-software/trevi-hr + +|badge1| |badge2| |badge3| + +Apply Employee Benefits to Payroll +================================== +* Benefits can be linked to payroll + * Choose the 'Allowance' type for earnings + * Choose the 'Deduction' type for premiums + * Use Salary Rules to integrate them in to Payroll Structures + * Information about the benefits are available to Salary Rules in a top-level 'benefits' object + - Example: result = hr_benefit..deductions + * The available fields are: + - qty: the number of policies of this type found + - earnings: the money to add + - deductions: the money to deduct + - ppf (Percentage Payroll Factor): 1 if the policy was active for the entire payroll period; less than 1 if it was active only for a fraction of the payroll period + +**Table of contents** + +.. contents:: + :local: + +Changelog +========= + +14.0.1.2.1 (2022-08-17) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [FIX] Catch up to changes in OCA/Payroll. Update your salary rules to take into account the dictionary naming changes. + +14.0.1.0.1 (2022-08-09) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [FIX] Slightly off PPF calculation of benefit policy when the payslip period lengths don't equal the policy period lengths. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* TREVI Software +* Michael Telahun Makonnen + +Other credits +~~~~~~~~~~~~~ + +* Michael Telahun Makonnen + +Maintainers +~~~~~~~~~~~ + +This module is part of the `trevi-software/trevi-hr `_ project on GitHub. + +You are welcome to contribute. diff --git a/hr_benefit_payroll/__init__.py b/hr_benefit_payroll/__init__.py new file mode 100644 index 00000000..f4515d9a --- /dev/null +++ b/hr_benefit_payroll/__init__.py @@ -0,0 +1,5 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013,2014 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import models diff --git a/hr_benefit_payroll/__manifest__.py b/hr_benefit_payroll/__manifest__.py new file mode 100644 index 00000000..adf04344 --- /dev/null +++ b/hr_benefit_payroll/__manifest__.py @@ -0,0 +1,29 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013,2014 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + "name": "Benefits Payroll", + "summary": "Access benefits in payroll through salary rules.", + "version": "15.0.1.0.0", + "category": "Payroll", + "author": "TREVI Software, Michael Telahun Makonnen", + "license": "AGPL-3", + "images": ["static/src/img/main_screenshot.png"], + "website": "https://github.com/trevi-software/trevi-hr", + "depends": [ + "hr_benefit", + "payroll", + ], + "data": [ + "security/ir.model.access.csv", + "security/security.xml", + "views/benefit_view.xml", + "views/benefit_policy_view.xml", + "views/benefit_premium_payment_view.xml", + "views/hr_employee_view.xml", + "views/hr_payslip_view.xml", + "views/hr_salary_rule_view.xml", + ], + "installable": True, +} diff --git a/hr_benefit_payroll/i18n/hr_benefit_payroll.pot b/hr_benefit_payroll/i18n/hr_benefit_payroll.pot new file mode 100644 index 00000000..3bb28c3d --- /dev/null +++ b/hr_benefit_payroll/i18n/hr_benefit_payroll.pot @@ -0,0 +1,302 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_benefit_payroll +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_benefit_premium_payment__amount +msgid "Amount" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_payslip_line__benefit_id +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_salary_rule__benefit_id +msgid "Benefit" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model,name:hr_benefit_payroll.model_hr_benefit_policy +msgid "Benefit Enrollment" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_payslip__premium_payment_ids +#: model_terms:ir.ui.view,arch_db:hr_benefit_payroll.view_hr_payslip_form +msgid "Benefit Premium Payments" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model,name:hr_benefit_payroll.model_hr_benefit_premium_payment +msgid "Benefit premium payment" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_payslip__benefit_line_ids +#: model_terms:ir.ui.view,arch_db:hr_benefit_payroll.view_hr_payslip_form +msgid "Benefits" +msgstr "" + +#. module: hr_benefit_payroll +#: model_terms:ir.ui.view,arch_db:hr_benefit_payroll.view_premium_payments_form +msgid "Cancel" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields.selection,name:hr_benefit_payroll.selection__hr_benefit_premium_payment__state__cancel +msgid "Cancelled" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_payslip_benefit__code +msgid "Code" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_benefit_premium_payment__create_uid +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_payslip_benefit__create_uid +msgid "Created by" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_benefit_premium_payment__create_date +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_payslip_benefit__create_date +msgid "Created on" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_benefit_premium_payment__date +#: model_terms:ir.ui.view,arch_db:hr_benefit_payroll.view_premium_payments_filter +msgid "Date" +msgstr "" + +#. module: hr_benefit_payroll +#: model_terms:ir.ui.view,arch_db:hr_benefit_payroll.view_premium_payments_form +msgid "Decline" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_payslip_benefit__deductions +msgid "Deductions" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_payslip_benefit__name +msgid "Description" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_benefit__display_name +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_benefit_policy__display_name +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_benefit_premium_payment__display_name +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_payslip__display_name +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_payslip_benefit__display_name +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_salary_rule__display_name +msgid "Display Name" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields.selection,name:hr_benefit_payroll.selection__hr_benefit_premium_payment__state__done +msgid "Done" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields.selection,name:hr_benefit_payroll.selection__hr_benefit_premium_payment__state__draft +msgid "Draft" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_payslip_benefit__earnings +msgid "Earnings" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_benefit_premium_payment__employee_id +#: model_terms:ir.ui.view,arch_db:hr_benefit_payroll.view_premium_payments_filter +msgid "Employee" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model,name:hr_benefit_payroll.model_hr_benefit +msgid "Employee Benefit" +msgstr "" + +#. module: hr_benefit_payroll +#: code:addons/hr_benefit_payroll/models/hr_payslip.py:0 +#, python-format +msgid "" +"Error creating benefit premium payment records!Unable to find a valid benefit policy:\n" +"Employee: %s\n" +"Benefit: %s" +msgstr "" + +#. module: hr_benefit_payroll +#: model_terms:ir.ui.view,arch_db:hr_benefit_payroll.view_premium_payments_filter +msgid "Group By..." +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_benefit__id +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_benefit_policy__id +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_benefit_premium_payment__id +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_payslip__id +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_payslip_benefit__id +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_salary_rule__id +msgid "ID" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_benefit____last_update +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_benefit_policy____last_update +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_benefit_premium_payment____last_update +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_payslip____last_update +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_payslip_benefit____last_update +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_salary_rule____last_update +msgid "Last Modified on" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_benefit_premium_payment__write_uid +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_payslip_benefit__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_benefit_premium_payment__write_date +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_payslip_benefit__write_date +msgid "Last Updated on" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_benefit__link2payroll +msgid "Link to Payroll" +msgstr "" + +#. module: hr_benefit_payroll +#: model_terms:ir.ui.view,arch_db:hr_benefit_payroll.view_premium_payments_filter +msgid "My Payments" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_payslip_benefit__payslip_id +msgid "Pay Slip" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_payslip_benefit__ppf +msgid "Payroll Period Factor" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model,name:hr_benefit_payroll.model_hr_payslip +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_benefit_premium_payment__payslip_id +msgid "Payslip" +msgstr "" + +#. module: hr_benefit_payroll +#: model_terms:ir.ui.view,arch_db:hr_benefit_payroll.view_hr_payslip_form +msgid "Payslip Benefit Lines" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model,name:hr_benefit_payroll.model_hr_payslip_benefit +msgid "Payslip Benefits" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields.selection,name:hr_benefit_payroll.selection__hr_benefit_premium_payment__state__pending +#: model_terms:ir.ui.view,arch_db:hr_benefit_payroll.view_premium_payments_form +msgid "Pending" +msgstr "" + +#. module: hr_benefit_payroll +#: code:addons/hr_benefit_payroll/models/benefit_premium_payment.py:0 +#, python-format +msgid "" +"Permission DeniedYou may not delete a premium payment that is past the \"draft\" stage.\n" +"Policy: %s\n" +"Payment Date: %s" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_benefit_premium_payment__policy_id +#: model_terms:ir.ui.view,arch_db:hr_benefit_payroll.view_premium_payments_filter +msgid "Policy" +msgstr "" + +#. module: hr_benefit_payroll +#: model_terms:ir.ui.view,arch_db:hr_benefit_payroll.view_premium_payments_form +msgid "Premium Payment" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.actions.act_window,name:hr_benefit_payroll.open_premium_payments_view +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_benefit_policy__premium_payment_ids +#: model:ir.ui.menu,name:hr_benefit_payroll.menu_premium_payments +#: model_terms:ir.ui.view,arch_db:hr_benefit_payroll.view_benefits_policy_form +#: model_terms:ir.ui.view,arch_db:hr_benefit_payroll.view_hr_payslip_form +#: model_terms:ir.ui.view,arch_db:hr_benefit_payroll.view_premium_payments_tree +msgid "Premium Payments" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_payslip_line__has_premium_payment +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_salary_rule__has_premium_payment +msgid "Premium payment?" +msgstr "" + +#. module: hr_benefit_payroll +#: model_terms:ir.ui.view,arch_db:hr_benefit_payroll.view_benefits_policy_form +msgid "Premiums" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_payslip_benefit__qty +msgid "Quantity" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model,name:hr_benefit_payroll.model_hr_salary_rule +msgid "Salary Rule" +msgstr "" + +#. module: hr_benefit_payroll +#: model_terms:ir.ui.view,arch_db:hr_benefit_payroll.view_premium_payments_filter +msgid "Search All Payments" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_payslip_benefit__sequence +msgid "Sequence" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,field_description:hr_benefit_payroll.field_hr_benefit_premium_payment__state +msgid "State" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,help:hr_benefit_payroll.field_hr_payslip_benefit__code +msgid "The code used in the salary rules" +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,help:hr_benefit_payroll.field_hr_payslip_benefit__qty +msgid "The number of policies of this benefit that apply to this payslip." +msgstr "" + +#. module: hr_benefit_payroll +#: model:ir.model.fields,help:hr_benefit_payroll.field_hr_payslip_benefit__ppf +msgid "" +"The percentage of this benefit applied to the payslip. Directly related to " +"the percentage of the contract that applies to the payslip." +msgstr "" diff --git a/hr_benefit_payroll/models/__init__.py b/hr_benefit_payroll/models/__init__.py new file mode 100644 index 00000000..6a2f8619 --- /dev/null +++ b/hr_benefit_payroll/models/__init__.py @@ -0,0 +1,10 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013,2014 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import benefit +from . import benefit_policy +from . import benefit_premium_payment +from . import hr_payslip +from . import hr_payslip_benefits +from . import hr_salary_rule diff --git a/hr_benefit_payroll/models/benefit.py b/hr_benefit_payroll/models/benefit.py new file mode 100644 index 00000000..3043671e --- /dev/null +++ b/hr_benefit_payroll/models/benefit.py @@ -0,0 +1,12 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013,2014 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class Benefit(models.Model): + + _inherit = "hr.benefit" + + link2payroll = fields.Boolean(string="Link to Payroll") diff --git a/hr_benefit_payroll/models/benefit_policy.py b/hr_benefit_payroll/models/benefit_policy.py new file mode 100644 index 00000000..700d2aef --- /dev/null +++ b/hr_benefit_payroll/models/benefit_policy.py @@ -0,0 +1,73 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013,2014 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import math + +from odoo import fields, models + + +class BenefitPolicy(models.Model): + + _inherit = "hr.benefit.policy" + + premium_payment_ids = fields.One2many( + string="Premium Payments", + comodel_name="hr.benefit.premium.payment", + inverse_name="policy_id", + readonly=True, + ) + + def calculate_advantage(self, dE): + + self.ensure_one() + adv_amount = 0 + adv = self.benefit_id.get_latest_advantage(dE) + if self.advantage_override: + adv_amount = self.advantage_amount + elif adv: + if adv.type == "allowance": + adv_amount = adv.allowance_amount + elif adv.type == "loan": + adv_amount = adv.loan_amount + return adv_amount + + def _get_paid_amount(self): + + self.ensure_one() + res = 0 + for payment in self.premium_payment_ids: + if payment.state not in ["draft", "cancel"]: + res += payment.amount + return res + + def calculate_premium(self, dE, annual_pay_periods, refund=False): + + self.ensure_one() + prm_amount = 0 + prm = self.benefit_id.get_latest_premium(dE) + paid = self._get_paid_amount() + if refund: + payments = self.premium_payment_ids.filtered( + lambda r: r.date <= dE and r.state not in ["draft", "cancel"] + ).sorted("date") + prm_amount = len(payments) > 0 and payments[-1].amount or 0 + elif self.premium_override: + total = self.premium_override_total + prm_amount = self.premium_override_amount + if total: + prm_amount = ( + (total - paid) > prm_amount and prm_amount or (total - paid) + ) + if prm_amount < 0: + prm_amount = 0 + elif prm: + if prm.type == "annual": + prm_amount = math.floor(prm.amount / float(annual_pay_periods)) + else: + prm_amount = math.floor(prm.amount / float(annual_pay_periods / 12)) + if prm.total_amount and (prm.total_amount - paid) < prm_amount: + prm_amount = ( + (prm.total_amount - paid) > 0 and (prm.total_amount - paid) or 0 + ) + return prm_amount diff --git a/hr_benefit_payroll/models/benefit_premium_payment.py b/hr_benefit_payroll/models/benefit_premium_payment.py new file mode 100644 index 00000000..ec10c0e6 --- /dev/null +++ b/hr_benefit_payroll/models/benefit_premium_payment.py @@ -0,0 +1,102 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013,2014 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import _, fields, models +from odoo.exceptions import UserError + + +class PremiumPayment(models.Model): + + _name = "hr.benefit.premium.payment" + _description = "Benefit premium payment" + _rec_name = "date" + + def _compute_policy_id_domain(self): + policy_ids = self.env["hr.benefit.policy"].search( + [ + ("employee_id", "in", self.mapped("employee_id").ids), + ] + ) + policy_ids = policy_ids.filtered(lambda rec: rec.benefit_id.has_premium) + return [("id", "in", policy_ids.ids)] + + date = fields.Date(required=True, default=fields.Date.today()) + policy_id = fields.Many2one( + string="Policy", + comodel_name="hr.benefit.policy", + domain=_compute_policy_id_domain, + required=True, + readonly=True, + states={"draft": [("readonly", False)]}, + ) + employee_id = fields.Many2one( + string="Employee", + comodel_name="hr.employee", + required=True, + readonly=True, + states={"draft": [("readonly", False)]}, + ) + amount = fields.Float( + digits="Account", + required=True, + readonly=True, + states={"draft": [("readonly", False)]}, + ) + payslip_id = fields.Many2one( + string="Payslip", comodel_name="hr.payslip", ondelete="cascade" + ) + state = fields.Selection( + selection=[ + ("draft", "Draft"), + ("pending", "Pending"), + ("cancel", "Cancelled"), + ("done", "Done"), + ], + readonly=True, + default="draft", + ) + + def name_get(self): + + res = [] + for payment in self: + res.append( + ( + payment.id, + "{} {} {}".format( + payment.policy_id.name, payment.date, payment.amount + ), + ) + ) + + return res + + def unlink(self): + + for payment in self: + if payment.state != "draft" and not ( + payment.env.context and payment.env.context.get("force_delete", False) + ): + raise UserError( + _( + "Permission Denied" + 'You may not delete a premium payment that is past the "draft" stage.' + "\nPolicy: %s\nPayment Date: %s" + ) + % (payment.policy_id.name, payment.date) + ) + + return super(PremiumPayment, self).unlink() + + def state_pending(self): + + return self.write({"state": "pending"}) + + def state_done(self): + + return self.write({"state": "done"}) + + def state_cancel(self): + + return self.write({"state": "cancel"}) diff --git a/hr_benefit_payroll/models/hr_payslip.py b/hr_benefit_payroll/models/hr_payslip.py new file mode 100644 index 00000000..ed2909c4 --- /dev/null +++ b/hr_benefit_payroll/models/hr_payslip.py @@ -0,0 +1,261 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013,2014 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from datetime import timedelta + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + +from odoo.addons.payroll.models.hr_payslip import BaseBrowsableObject + + +class HrPayslip(models.Model): + + _inherit = "hr.payslip" + + benefit_line_ids = fields.One2many( + string="Benefits", comodel_name="hr.payslip.benefit", inverse_name="payslip_id" + ) + premium_payment_ids = fields.One2many( + string="Benefit Premium Payments", + comodel_name="hr.benefit.premium.payment", + inverse_name="payslip_id", + ) + + @api.onchange("date_from", "date_to") + def onchange_dates(self): + super().onchange_dates() + for rec in self: + if not rec.date_from or not rec.date_to: + continue + rec.benefit_line_ids.unlink() + benefit_lines_dict = rec.get_benefit_lines( + rec._get_employee_contracts(), rec.date_from, rec.date_to + ) + write_vals = [(0, 0, v) for _k, v in benefit_lines_dict.items()] + rec.write({"benefit_line_ids": write_vals}) + + def get_payslip_vals( + self, date_from, date_to, employee_id=False, contract_id=False, struct_id=False + ): + res = super().get_payslip_vals( + date_from, date_to, employee_id, contract_id, struct_id + ) + + # Delete old lines + res["value"].update( + {"benefit_line_ids": [(2, x) for x in self.benefit_line_ids.ids]} + ) + + # Get benefit lines + contracts = self._get_employee_contracts() + benefit_lines = self.get_benefit_lines(contracts, date_from, date_to) + res["value"].update( + { + "benefit_line_ids": benefit_lines, + } + ) + return res + + @api.model + def get_benefit_lines(self, contracts, date_from, date_to, credit_note=False): + + res = {} + if not contracts: + return res + employee = contracts[0].employee_id + + # Search policies for those employees that are linked to payroll + policy_ids = self.env["hr.benefit.policy"].search( + [ + ("employee_id", "=", employee.id), + ("benefit_id.link2payroll", "=", True), + ("start_date", "<=", date_to), + "|", + ("end_date", "=", False), + ("end_date", ">=", date_from), + ] + ) + benefit_ids = policy_ids.mapped("benefit_id") + for bd in benefit_ids: + res.update( + { + bd.code: { + "code": bd.code, + "name": bd.name, + "qty": 0, + "ppf": 0, + "earnings": 0, + "deductions": 0, + } + } + ) + + # One dict per benefit + # + dSlipStart = date_from + dSlipEnd = date_to + d = dSlipStart + deltaSlip = 0 + while d <= dSlipEnd: + deltaSlip += 1 + d += timedelta(days=+1) + + # Test for installation of payroll_period + if hasattr(contracts[0], "annual_pay_periods"): + app = contracts[0].annual_pay_periods + else: + app = 12 + + for policy in policy_ids: + + # Calculate partial period factor relative to the policy + dPolStart = policy.start_date + dPolEnd = dSlipEnd + if policy.end_date: + dPolEnd = policy.end_date + dS = (dPolStart > dSlipStart) and dPolStart or dSlipStart + dE = (dPolEnd < dSlipEnd) and dPolEnd or dSlipEnd + + if (dPolEnd <= dSlipStart) or (dPolStart >= dSlipEnd): + continue + + d = dS + deltaPol = 0 + while d <= dE: + deltaPol += 1 + d += timedelta(days=+1) + + # Calculate advantage + # + adv_amount = policy.calculate_advantage(dE) + + # Calculate premium + # + prm_amount = policy.calculate_premium(dE, app, refund=credit_note) + + res[policy.benefit_id.code]["qty"] += 1 + res[policy.benefit_id.code]["ppf"] += float(deltaPol) / float(deltaSlip) + res[policy.benefit_id.code]["earnings"] += adv_amount + res[policy.benefit_id.code]["deductions"] += prm_amount + + return res + + def action_payslip_done(self): + res = super().action_payslip_done() + + for payslip in self: + policy_ids = self.env["hr.benefit.policy"].search( + [ + ("employee_id", "=", payslip.employee_id.id), + ("benefit_id.link2payroll", "=", True), + ("start_date", "<=", payslip.date_to), + "|", + ("end_date", "=", False), + ("end_date", ">=", payslip.date_from), + ] + ) + benefits = {} + for p in policy_ids: + benefits.update( + {p.name: {"id": p.benefit_id.id, "amount": p.premium_amount}} + ) + payslip.record_benefit_premium_payments(benefits) + payslip.finalize_benefit_premium_payments() + + return res + + def record_benefit_premium_payments(self, benefits): + + policy_obj = self.env["hr.benefit.policy"] + premium_obj = self.env["hr.benefit.premium.payment"] + for payslip in self: + for k, v in benefits.items(): + pol_ids = policy_obj.search( + [ + ("employee_id", "=", payslip.employee_id.id), + ("benefit_id", "=", v["id"]), + ] + ) + if len(pol_ids) == 0: + UserError( + _( + "Error creating benefit premium payment records!" + "Unable to find a valid benefit policy:\nEmployee: %s\nBenefit: %s" + ) + % (payslip.employee_id.name, k) + ) + + premium_obj.create( + { + "payslip_id": payslip.id, + "date": payslip.date_to, + "employee_id": payslip.employee_id.id, + "policy_id": pol_ids[0].id, + "amount": payslip.credit_note and -v["amount"] or v["amount"], + } + ) + return + + def remove_benefit_premium_payments(self): + + pay_obj = self.env["hr.benefit.premium.payment"] + pay_ids = pay_obj.search([("payslip_id", "in", self.ids)]) + pay_ids.unlink() + + return + + def finalize_benefit_premium_payments(self): + + payments = self.mapped("premium_payment_ids") + payments.state_done() + + def refund_sheet(self): + + res = super(HrPayslip, self).refund_sheet() + payments = self.mapped("premium_payment_ids") + if payments: + payments.state_cancel() + + return res + + def get_benefits_dictionary(self, contracts): + """ + @return: returns a dictionary dic: + * hr_benefit..qty - the number policies for this benefit + * hr_benefit..ppf - the ppf of policy with respect to payslip + * hr_benefit..deductions - the amount to deduct or 0 + * hr_benefit..earnings - the earning amount or 0 + """ + + self.ensure_one() + res = {} + + for line in self.benefit_line_ids: + res.update( + { + line.code: BaseBrowsableObject( + { + "qty": line.qty, + "ppf": line.ppf, + "deductions": line.deductions, + "earnings": line.earnings, + } + ) + } + ) + + return res + + def _get_baselocaldict(self, contracts): + + res = super()._get_baselocaldict(contracts) + res.update( + { + "hr_benefit": BaseBrowsableObject( + self.get_benefits_dictionary(contracts) + ), + } + ) + return res diff --git a/hr_benefit_payroll/models/hr_payslip_benefits.py b/hr_benefit_payroll/models/hr_payslip_benefits.py new file mode 100644 index 00000000..620eb5ad --- /dev/null +++ b/hr_benefit_payroll/models/hr_payslip_benefits.py @@ -0,0 +1,28 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class HrPayslipWorkedDays(models.Model): + _name = "hr.payslip.benefit" + _description = "Payslip Benefits" + _order = "payslip_id, sequence" + + name = fields.Char(string="Description", required=True) + payslip_id = fields.Many2one( + "hr.payslip", "Pay Slip", required=True, ondelete="cascade", index=True + ) + code = fields.Char(required=True, help="The code used in the salary rules") + sequence = fields.Integer(required=True, index=True, default=10) + qty = fields.Integer( + "Quantity", + help="The number of policies of this benefit that apply to this payslip.", + ) + ppf = fields.Float( + "Payroll Period Factor", + help="The percentage of this benefit applied to the payslip. Directly " + "related to the percentage of the contract that applies to the payslip.", + ) + earnings = fields.Float() + deductions = fields.Float() diff --git a/hr_benefit_payroll/models/hr_salary_rule.py b/hr_benefit_payroll/models/hr_salary_rule.py new file mode 100644 index 00000000..884d94dc --- /dev/null +++ b/hr_benefit_payroll/models/hr_salary_rule.py @@ -0,0 +1,13 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013,2014 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class HrSalaryRule(models.Model): + + _inherit = "hr.salary.rule" + + benefit_id = fields.Many2one(string="Benefit", comodel_name="hr.benefit") + has_premium_payment = fields.Boolean(string="Premium payment?") diff --git a/hr_benefit_payroll/readme/CREDITS.rst b/hr_benefit_payroll/readme/CREDITS.rst new file mode 100644 index 00000000..d264bc7e --- /dev/null +++ b/hr_benefit_payroll/readme/CREDITS.rst @@ -0,0 +1 @@ +* Michael Telahun Makonnen diff --git a/hr_benefit_payroll/readme/DESCRIPTION.rst b/hr_benefit_payroll/readme/DESCRIPTION.rst new file mode 100644 index 00000000..2f84b8c4 --- /dev/null +++ b/hr_benefit_payroll/readme/DESCRIPTION.rst @@ -0,0 +1,13 @@ +Apply Employee Benefits to Payroll +================================== +* Benefits can be linked to payroll + * Choose the 'Allowance' type for earnings + * Choose the 'Deduction' type for premiums + * Use Salary Rules to integrate them in to Payroll Structures + * Information about the benefits are available to Salary Rules in a top-level 'benefits' object + - Example: result = hr_benefit..deductions + * The available fields are: + - qty: the number of policies of this type found + - earnings: the money to add + - deductions: the money to deduct + - ppf (Percentage Payroll Factor): 1 if the policy was active for the entire payroll period; less than 1 if it was active only for a fraction of the payroll period diff --git a/hr_benefit_payroll/readme/HISTORY.rst b/hr_benefit_payroll/readme/HISTORY.rst new file mode 100644 index 00000000..fdac68cc --- /dev/null +++ b/hr_benefit_payroll/readme/HISTORY.rst @@ -0,0 +1,9 @@ +14.0.1.2.1 (2022-08-17) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [FIX] Catch up to changes in OCA/Payroll. Update your salary rules to take into account the dictionary naming changes. + +14.0.1.0.1 (2022-08-09) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [FIX] Slightly off PPF calculation of benefit policy when the payslip period lengths don't equal the policy period lengths. diff --git a/hr_benefit_payroll/security/ir.model.access.csv b/hr_benefit_payroll/security/ir.model.access.csv new file mode 100644 index 00000000..e38e6d07 --- /dev/null +++ b/hr_benefit_payroll/security/ir.model.access.csv @@ -0,0 +1,10 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_hr_benefit_pu,access_hr_benefit,hr_benefit.model_hr_benefit,payroll.group_payroll_user,1,0,0,0 +access_hr_benefit_policy_pu,access_hr_benefit_policy,hr_benefit.model_hr_benefit_policy,payroll.group_payroll_user,1,0,0,0 +access_hr_benefit_premium_pu,access_hr_benefit_policy_premium,hr_benefit.model_hr_benefit_premium,payroll.group_payroll_user,1,0,0,0 +access_hr_benefit_premium_payment_all,access_hr_benefit_policy_premium_payment,model_hr_benefit_premium_payment,base.group_user,1,0,0,0 +access_hr_benefit_premium_payment_user,access_hr_benefit_policy_premium_payment,model_hr_benefit_premium_payment,hr.group_hr_user,1,0,0,0 +access_hr_benefit_premium_payment_pm,access_hr_benefit_policy_premium_payment,model_hr_benefit_premium_payment,payroll.group_payroll_user,1,1,1,1 +access_hr_benefit_claim_pu,access_hr_benefit_claim,hr_benefit.model_hr_benefit_claim,payroll.group_payroll_user,1,0,0,0 +access_hr_benefit_adv_pu,access_hr_benefit_policy_adv,hr_benefit.model_hr_benefit_advantage,payroll.group_payroll_user,1,0,0,0 +access_hr_payslip_benefit_user,access_hr_payslip_benefit,hr_benefit_payroll.model_hr_payslip_benefit,payroll.group_payroll_user,1,1,1,1 diff --git a/hr_benefit_payroll/security/security.xml b/hr_benefit_payroll/security/security.xml new file mode 100644 index 00000000..fad085e2 --- /dev/null +++ b/hr_benefit_payroll/security/security.xml @@ -0,0 +1,26 @@ + + + + + + Benefit Premium Payment: Own and Subordinates + + + ['|',('employee_id.user_id', '=', user.id), + ('employee_id.department_id.manager_id.user_id', '=', user.id)] + + + + + + Benefit Policy: Payroll Officer can see all + + + + + [(1, '=', 1)] + + + + + diff --git a/hr_benefit_payroll/static/description/icon.png b/hr_benefit_payroll/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/hr_benefit_payroll/static/description/icon.png differ diff --git a/hr_benefit_payroll/static/description/index.html b/hr_benefit_payroll/static/description/index.html new file mode 100644 index 00000000..775e82fc --- /dev/null +++ b/hr_benefit_payroll/static/description/index.html @@ -0,0 +1,454 @@ + + + + + + +Benefits Payroll + + + +
+

Benefits Payroll

+ + +

Beta License: AGPL-3 trevi-software/trevi-hr

+
+

Apply Employee Benefits to Payroll

+
    +
  • +
    Benefits can be linked to payroll
    +
      +
    • Choose the ‘Allowance’ type for earnings
    • +
    • Choose the ‘Deduction’ type for premiums
    • +
    • Use Salary Rules to integrate them in to Payroll Structures
    • +
    • +
      Information about the benefits are available to Salary Rules in a top-level ‘benefits’ object
      +
        +
      • Example: result = hr_benefit.<CODE>.deductions
      • +
      +
      +
      +
    • +
    • +
      The available fields are:
      +
        +
      • qty: the number of policies of this type found
      • +
      • earnings: the money to add
      • +
      • deductions: the money to deduct
      • +
      • ppf (Percentage Payroll Factor): 1 if the policy was active for the entire payroll period; less than 1 if it was active only for a fraction of the payroll period
      • +
      +
      +
      +
    • +
    +
    +
    +
  • +
+

Table of contents

+
+
+

Changelog

+
+

14.0.1.2.1 (2022-08-17)

+
    +
  • [FIX] Catch up to changes in OCA/Payroll. Update your salary rules to take into account the dictionary naming changes.
  • +
+
+
+

14.0.1.0.1 (2022-08-09)

+
    +
  • [FIX] Slightly off PPF calculation of benefit policy when the payslip period lengths don’t equal the policy period lengths.
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • TREVI Software
  • +
  • Michael Telahun Makonnen
  • +
+
+
+

Other credits

+ +
+
+

Maintainers

+

This module is part of the trevi-software/trevi-hr project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/hr_benefit_payroll/tests/__init__.py b/hr_benefit_payroll/tests/__init__.py new file mode 100644 index 00000000..9655609b --- /dev/null +++ b/hr_benefit_payroll/tests/__init__.py @@ -0,0 +1,7 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import test_benefit_access +from . import test_benefit_policy +from . import test_benefit_premium_payment +from . import test_hr_payslip diff --git a/hr_benefit_payroll/tests/test_benefit_access.py b/hr_benefit_payroll/tests/test_benefit_access.py new file mode 100644 index 00000000..39f044cf --- /dev/null +++ b/hr_benefit_payroll/tests/test_benefit_access.py @@ -0,0 +1,97 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from datetime import date + +from odoo.tests import new_test_user + +from odoo.addons.hr_benefit.tests import common as benefit_common + + +class TestBenefit(benefit_common.TestBenefitCommon): + @classmethod + def setUpClass(cls): + super(TestBenefit, cls).setUpClass() + + cls.PremiumPayment = cls.env["hr.benefit.premium.payment"] + # Payroll Manager user + cls.userPM = new_test_user( + cls.env, + login="pm", + groups="base.group_user,payroll.group_payroll_manager", + name="Payroll manager", + ) + # Payroll user + cls.userPU = new_test_user( + cls.env, + login="pu", + groups="base.group_user,payroll.group_payroll_user", + name="Payroll manager", + ) + + def test_policy_access(self): + """ + hr.benefit.policy access: Payroll Mgr - read-only, Payroll User read-only + """ + + bn1 = self.create_benefit(self.benefit_create_vals) + bn2 = self.create_benefit( + { + "name": "BenefitB", + "code": "B", + "multi_policy": True, + } + ) + pol = self.create_policy(self.eeJohn, bn1, date.today()) + + # Mgr + # Succeeds because of payroll user rights + self.create_succeeds( + self.userPM, + self.Policy, + { + "name": "tbp", + "employee_id": self.eeJohn.id, + "benefit_id": bn2.id, + "start_date": date.today(), + }, + ) + self.unlink_fails(self.userPM, pol) + self.read_succeeds(self.userPM, self.Policy, pol.id) + self.write_succeeds(self.userPM, self.Policy, pol.id, {"name": "tbp2"}) + + # User + # addons/payroll where payroll officer is created implies group: HR Officer + self.create_succeeds( + self.userPU, + self.Policy, + { + "name": "tbp", + "employee_id": self.eeJohn.id, + "benefit_id": bn2.id, + "start_date": date.today(), + }, + ) + self.unlink_fails(self.userPU, pol) + self.read_succeeds(self.userPU, self.Policy, pol.id) + self.write_succeeds(self.userPU, self.Policy, pol.id, {"name": "tbp2"}) + + def test_policy_user_own(self): + """A user can only read his/her own policies""" + + bn1 = self.create_benefit(self.benefit_create_vals) + polJohn = self.create_policy(self.eeJohn, bn1, date.today()) + polPaul = self.create_policy(self.eePaul, bn1, date.today()) + grpOfficer = self.env.ref("hr.group_hr_user") + self.assertNotIn(grpOfficer, self.userJohn.groups_id) + self.assertNotEqual(polJohn, polPaul) + self.assertEqual(self.userJohn, polJohn.employee_id.user_id) + self.assertEqual(self.userPaul, polPaul.employee_id.user_id) + + # John can read his own policy + self.read_succeeds(self.userJohn, self.Policy, polJohn.id) + # but not Paul's + self.read_fails(self.userJohn, self.Policy, polPaul.id) + # John can't modify his own policy or Paul's + self.write_fails(self.userJohn, self.Policy, polJohn.id, {"note": "A"}) + self.write_fails(self.userJohn, self.Policy, polPaul.id, {"note": "A"}) diff --git a/hr_benefit_payroll/tests/test_benefit_policy.py b/hr_benefit_payroll/tests/test_benefit_policy.py new file mode 100644 index 00000000..76ac7cb5 --- /dev/null +++ b/hr_benefit_payroll/tests/test_benefit_policy.py @@ -0,0 +1,253 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from datetime import date + +from dateutil.relativedelta import relativedelta + +from odoo.addons.hr_benefit.tests import common as benefit_common + + +class TestBenefit(benefit_common.TestBenefitCommon): + @classmethod + def setUpClass(cls): + super(TestBenefit, cls).setUpClass() + + cls.PremiumPayment = cls.env["hr.benefit.premium.payment"] + + def test_benefit_policy_allowance_default_amount(self): + + today = date.today() + bn = self.create_benefit(self.benefit_create_vals) + self.create_earning(bn, start=today, allowance=1000) + pol = self.create_policy(self.eeJohn, bn, start=today) + pol.state_open() + + self.assertEqual( + pol.calculate_advantage(today), + 1000, + "Calculated advantage must be benefit amount.", + ) + + def test_benefit_policy_allowance_override_amount(self): + + today = date.today() + bn = self.create_benefit(self.benefit_create_vals) + self.create_earning(bn, start=today, allowance=1000) + pol = self.create_policy(self.eeJohn, bn, start=today, advantage=4400) + pol.state_open() + + self.assertEqual( + pol.calculate_advantage(today), + 4400, + "Calculated advantage must be override amount from policy.", + ) + + def test_benefit_policy_loan_amount(self): + + today = date.today() + bn = self.create_benefit(self.benefit_create_vals) + self.create_earning(bn, start=today, ptype="loan", loan=15000) + pol = self.create_policy(self.eeJohn, bn, start=today) + pol.state_open() + + self.assertEqual( + pol.calculate_advantage(today), + 15000, + "Calculated loan amount must match amount from earning creation.", + ) + + def test_benefit_policy_first_premium(self): + + bn = self.create_benefit(self.benefit_create_vals) + self.create_premium( + bn, + date.today() - relativedelta(days=1), + ptype="monthly", + amount=100, + total=300, + ) + pol = self.create_policy(self.eeJohn, bn, date.today()) + + self.assertEqual( + 100, + pol.calculate_premium(date.today(), 12), + "Initial premium amount and calculated premium should match", + ) + + def test_benefit_policy_last_premium(self): + + today = date.today() + policy_start = today - relativedelta(days=4) + dtPayment1 = today - relativedelta(days=3) + dtPayment2 = today - relativedelta(days=2) + dtPayment3 = today - relativedelta(days=1) + bn = self.create_benefit(self.benefit_create_vals) + self.create_premium( + bn, + date.today() - relativedelta(days=3), + ptype="monthly", + amount=100, + total=340, + ) + pol = self.create_policy(self.eeJohn, bn, policy_start) + # Make first 3 payments (with one additional cancelled payment) + self.PremiumPayment.create( + { + "employee_id": self.eeJohn.id, + "policy_id": pol.id, + "amount": pol.calculate_premium(dtPayment1, 12), + "date": dtPayment1, + } + ).state_cancel() + self.PremiumPayment.create( + { + "employee_id": self.eeJohn.id, + "policy_id": pol.id, + "amount": pol.calculate_premium(dtPayment1, 12), + "date": dtPayment1, + } + ).state_done() + self.PremiumPayment.create( + { + "employee_id": self.eeJohn.id, + "policy_id": pol.id, + "amount": pol.calculate_premium(dtPayment2, 12), + "date": dtPayment2, + } + ).state_done() + self.PremiumPayment.create( + { + "employee_id": self.eeJohn.id, + "policy_id": pol.id, + "amount": pol.calculate_premium(dtPayment3, 12), + "date": dtPayment3, + } + ).state_done() + + self.assertEqual( + len(pol.premium_payment_ids), + 4, + "There should be 4 payments (including the cancelled one)", + ) + self.assertEqual( + 40, + pol.calculate_premium(today, 12), + "The last payment must be the residual amount after previous payments", + ) + + def test_benefit_policy_premium_override(self): + + today = date.today() + policy_start = today - relativedelta(days=4) + dtPayment1 = today - relativedelta(days=3) + bn = self.create_benefit(self.benefit_create_vals) + self.create_premium( + bn, + date.today() - relativedelta(days=3), + ptype="monthly", + amount=100, + total=340, + ) + pol = self.create_policy( + self.eeJohn, bn, policy_start, premium=200, premium_total=340 + ) + # Make first 1 payments (with one additional payment in 'draft') + self.PremiumPayment.create( + { + "employee_id": self.eeJohn.id, + "policy_id": pol.id, + "amount": pol.calculate_premium(dtPayment1, 12), + "date": dtPayment1, + } + ) + self.PremiumPayment.create( + { + "employee_id": self.eeJohn.id, + "policy_id": pol.id, + "amount": pol.calculate_premium(dtPayment1, 12), + "date": dtPayment1, + } + ).state_done() + + self.assertEqual( + len(pol.premium_payment_ids), + 2, + "There should be 2 payments (including the 'draft' one)", + ) + self.assertEqual( + 140, + pol.calculate_premium(today, 12), + "The last payment must be the residual amount after previous payments", + ) + + def test_benefit_policy_premium_refund(self): + + today = date.today() + policy_start = today - relativedelta(days=4) + dtPayment1 = today - relativedelta(days=3) + dtPayment2 = today - relativedelta(days=2) + dtPayment3 = today - relativedelta(days=1) + bn = self.create_benefit(self.benefit_create_vals) + self.create_premium( + bn, + policy_start, + ptype="monthly", + amount=100, + total=340, + ) + pol = self.create_policy(self.eeJohn, bn, policy_start) + + # Try to refund cancelled payment + self.PremiumPayment.create( + { + "employee_id": self.eeJohn.id, + "policy_id": pol.id, + "amount": pol.calculate_premium(dtPayment1, 12), + "date": dtPayment1, + } + ).state_cancel() + self.assertEqual( + 0, + pol.calculate_premium(today, 12, refund=True), + "A Cancelled payment should not be refunded", + ) + + # Make all payments and try to refund last residual payment + self.PremiumPayment.create( + { + "employee_id": self.eeJohn.id, + "policy_id": pol.id, + "amount": pol.calculate_premium(dtPayment1, 12), + "date": dtPayment1, + } + ).state_done() + self.PremiumPayment.create( + { + "employee_id": self.eeJohn.id, + "policy_id": pol.id, + "amount": pol.calculate_premium(dtPayment2, 12), + "date": dtPayment2, + } + ).state_done() + self.PremiumPayment.create( + { + "employee_id": self.eeJohn.id, + "policy_id": pol.id, + "amount": pol.calculate_premium(dtPayment3, 12), + "date": dtPayment3, + } + ).state_done() + self.PremiumPayment.create( + { + "employee_id": self.eeJohn.id, + "policy_id": pol.id, + "amount": pol.calculate_premium(dtPayment3, 12), + "date": today, + } + ).state_done() + self.assertEqual( + 40, + pol.calculate_premium(today, 12, refund=True), + "The premium to be refunded must be the last payment made", + ) diff --git a/hr_benefit_payroll/tests/test_benefit_premium_payment.py b/hr_benefit_payroll/tests/test_benefit_premium_payment.py new file mode 100644 index 00000000..026b0d08 --- /dev/null +++ b/hr_benefit_payroll/tests/test_benefit_premium_payment.py @@ -0,0 +1,179 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo.exceptions import UserError +from odoo.tests import new_test_user + +from odoo.addons.hr_benefit.tests import common as benefit_common + + +class TestBenefit(benefit_common.TestBenefitCommon): + @classmethod + def setUpClass(cls): + super(TestBenefit, cls).setUpClass() + + cls.PremiumPayment = cls.env["hr.benefit.premium.payment"] + # Payroll Manager user + cls.userPM = new_test_user( + cls.env, + login="pm", + groups="base.group_user,payroll.group_payroll_manager", + name="Payroll manager", + ) + # Payroll user + cls.userPU = new_test_user( + cls.env, + login="pu", + groups="base.group_user,payroll.group_payroll_user", + name="Payroll manager", + ) + + def test_premium_payment_initial_state(self): + benefit = self.create_benefit(self.benefit_create_vals) + policy = self.create_policy(self.eeJohn, benefit) + pp = self.PremiumPayment.create( + { + "employee_id": self.eeJohn.id, + "policy_id": policy.id, + "amount": 100.00, + } + ) + self.assertEqual(pp.state, "draft", "Initial state must be 'draft'") + + def test_premium_payment_domain_policy_id(self): + benefit = self.create_benefit(self.benefit_create_vals) + self.create_premium(benefit, amount=100.00) + self.create_policy(self.eePaul, benefit) + policy = self.create_policy(self.eeJohn, benefit) + pp = self.PremiumPayment.create( + { + "employee_id": self.eeJohn.id, + "policy_id": policy.id, + "amount": 100.00, + } + ) + + domain = ( + self.env["hr.benefit.premium.payment"] + ._fields["policy_id"] + .get_domain_list(pp) + ) + domain_list = self.Policy.search(domain) + self.assertEqual( + len(domain_list), + 1, + "Only the policy for 'John' should be in the list of policies", + ) + self.assertIn( + policy, + domain_list, + "The policy for 'John' should be in the list of policies", + ) + + def test_premium_payment_unlink(self): + benefit = self.create_benefit(self.benefit_create_vals) + benefit.multi_policy = True + self.create_premium(benefit, amount=100.00) + polDraft = self.create_policy(self.eeJohn, benefit) + polPending = self.create_policy(self.eeJohn, benefit) + polCancel = self.create_policy(self.eeJohn, benefit) + polDone = self.create_policy(self.eeJohn, benefit) + ppDraft = self.PremiumPayment.create( + { + "employee_id": self.eeJohn.id, + "policy_id": polDraft.id, + "amount": 100.00, + } + ) + ppPending = self.PremiumPayment.create( + { + "employee_id": self.eeJohn.id, + "policy_id": polPending.id, + "amount": 100.00, + } + ) + ppPending.state_pending() + ppCancel = self.PremiumPayment.create( + { + "employee_id": self.eeJohn.id, + "policy_id": polCancel.id, + "amount": 100.00, + } + ) + ppCancel.state_cancel() + ppDone = self.PremiumPayment.create( + { + "employee_id": self.eeJohn.id, + "policy_id": polDone.id, + "amount": 100.00, + } + ) + ppDone.state_done() + + self.assertEqual(ppDraft.state, "draft") + try: + ppDraft.unlink() + except UserError: + self.fail("Unexpected UserError exception!") + + self.assertEqual(ppPending.state, "pending") + with self.assertRaises(UserError): + ppPending.unlink() + + self.assertEqual(ppCancel.state, "cancel") + with self.assertRaises(UserError): + ppCancel.unlink() + + self.assertEqual(ppDone.state, "done") + with self.assertRaises(UserError): + ppDone.unlink() + + def test_premium_payment_unlink_force_delete(self): + benefit = self.create_benefit(self.benefit_create_vals) + benefit.multi_policy = True + self.create_premium(benefit, amount=100.00) + polPending = self.create_policy(self.eeJohn, benefit) + polCancel = self.create_policy(self.eeJohn, benefit) + polDone = self.create_policy(self.eeJohn, benefit) + ppPending = self.PremiumPayment.create( + { + "employee_id": self.eeJohn.id, + "policy_id": polPending.id, + "amount": 100.00, + } + ) + ppPending.state_pending() + ppCancel = self.PremiumPayment.create( + { + "employee_id": self.eeJohn.id, + "policy_id": polCancel.id, + "amount": 100.00, + } + ) + ppCancel.state_cancel() + ppDone = self.PremiumPayment.create( + { + "employee_id": self.eeJohn.id, + "policy_id": polDone.id, + "amount": 100.00, + } + ) + ppDone.state_done() + + self.assertEqual(ppPending.state, "pending") + try: + ppPending.with_context(force_delete=True).unlink() + except UserError: + self.fail("Unexpected UserError exception!") + + self.assertEqual(ppCancel.state, "cancel") + try: + ppCancel.with_context(force_delete=True).unlink() + except UserError: + self.fail("Unexpected UserError exception!") + + self.assertEqual(ppDone.state, "done") + try: + ppDone.with_context(force_delete=True).unlink() + except UserError: + self.fail("Unexpected UserError exception!") diff --git a/hr_benefit_payroll/tests/test_hr_payslip.py b/hr_benefit_payroll/tests/test_hr_payslip.py new file mode 100644 index 00000000..c521d150 --- /dev/null +++ b/hr_benefit_payroll/tests/test_hr_payslip.py @@ -0,0 +1,380 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from datetime import date, timedelta + +from odoo import fields +from odoo.tests.common import Form + +from odoo.addons.hr_benefit.tests import common as benefit_common + + +class TestBenefit(benefit_common.TestBenefitCommon): + @classmethod + def setUpClass(cls): + super(TestBenefit, cls).setUpClass() + + cls.Payslip = cls.env["hr.payslip"] + cls.PayrollStructure = cls.env["hr.payroll.structure"] + cls.SalaryRule = cls.env["hr.salary.rule"] + cls.SalaryRuleCateg = cls.env["hr.salary.rule.category"] + + cls.categ_basic = cls.SalaryRuleCateg.create( + { + "name": "Basic", + "code": "BASIC", + } + ) + + cls.rule_basic = cls.SalaryRule.create( + { + "name": "Basic Salary", + "code": "BASIC", + "sequence": 1, + "category_id": cls.categ_basic.id, + "condition_select": "none", + "amount_select": "code", + "amount_python_compute": "result = contract.wage", + } + ) + + cls.payroll_structure = cls.PayrollStructure.create( + { + "name": "Salary Structure for Sales Person", + "code": "SP", + "company_id": cls.env.ref("base.main_company").id, + "rule_ids": [ + (4, cls.rule_basic.id), + ], + } + ) + + def create_contract( + self, eid, state, kanban_state, start, end=None, trial_end=None, pps_id=None + ): + return self.env["hr.contract"].create( + { + "name": "Contract", + "employee_id": eid, + "state": state, + "kanban_state": kanban_state, + "wage": 1, + "date_start": start, + "trial_date_end": trial_end, + "date_end": end, + "struct_id": self.payroll_structure.id, + } + ) + + def test_get_benefit_policies_for_payslip_premium(self): + + start = date(2021, 1, 1) + end = date(2021, 1, 31) + bvals = self.benefit_create_vals.copy() + bvals.update({"link2payroll": True}) + benefit = self.create_benefit(bvals) + self.create_premium(benefit, start=start, amount=100.00) + self.create_policy(self.eeJohn, benefit, start) + self.create_contract(self.eeJohn.id, "draft", "done", start).signal_confirm() + slip = self.Payslip.create( + { + "employee_id": self.eeJohn.id, + "date_from": start, + "date_to": end, + } + ) + slip.onchange_employee() + + self.assertEqual( + len(slip.benefit_line_ids), + 1, + "There should be only 1 benefit line attached to payslip", + ) + self.assertEqual( + slip.benefit_line_ids[0].earnings, 0, "Benefit line should show no earnings" + ) + self.assertEqual( + slip.benefit_line_ids[0].deductions, + 100, + "Benefit line should show a deduction of benefit premium amount", + ) + + def test_get_benefit_policies_for_payslip_premium_override(self): + + start = date(2021, 1, 1) + end = date(2021, 1, 31) + bvals = self.benefit_create_vals.copy() + bvals.update({"link2payroll": True}) + benefit = self.create_benefit(bvals) + self.create_premium(benefit, start=start, amount=100.00) + self.create_policy(self.eeJohn, benefit, start, premium=200.00) + self.create_contract(self.eeJohn.id, "draft", "done", start).signal_confirm() + slip = self.Payslip.create( + { + "employee_id": self.eeJohn.id, + "date_from": start, + "date_to": end, + } + ) + slip.onchange_employee() + + self.assertEqual( + len(slip.benefit_line_ids), + 1, + "There should be only 1 benefit line attached to payslip", + ) + self.assertEqual( + slip.benefit_line_ids[0].earnings, 0, "Benefit line should show no earnings" + ) + self.assertEqual( + slip.benefit_line_ids[0].deductions, + 200, + "Benefit line should show a deduction of the override amount", + ) + + def test_get_benefit_policies_for_payslip_premium_total(self): + + start = date(2021, 1, 1) + end = date(2021, 1, 31) + bvals = self.benefit_create_vals.copy() + bvals.update({"link2payroll": True}) + benefit = self.create_benefit(bvals) + self.create_premium(benefit, start=start, amount=100.00, total=500.00) + self.create_policy(self.eeJohn, benefit, start) + self.create_contract(self.eeJohn.id, "draft", "done", start).signal_confirm() + slip = self.Payslip.create( + { + "employee_id": self.eeJohn.id, + "date_from": start, + "date_to": end, + } + ) + slip.onchange_employee() + + self.assertEqual( + len(slip.benefit_line_ids), + 1, + "There should be only 1 benefit line attached to payslip", + ) + self.assertEqual( + slip.benefit_line_ids[0].earnings, 0, "Benefit line should show no earnings" + ) + self.assertEqual( + slip.benefit_line_ids[0].deductions, + 100, + "Benefit line should show a deduction of benefit premium amount", + ) + + def test_get_benefit_policies_for_payslip_allowance(self): + + start = date(2021, 1, 1) + end = date(2021, 1, 31) + bvals = self.benefit_create_vals.copy() + bvals.update({"link2payroll": True}) + benefit = self.create_benefit(bvals) + self.create_earning(benefit, start=start, allowance=500.00) + self.create_policy(self.eeJohn, benefit, start) + self.create_contract(self.eeJohn.id, "draft", "done", start).signal_confirm() + slip = self.Payslip.create( + { + "employee_id": self.eeJohn.id, + "date_from": start, + "date_to": end, + } + ) + slip.onchange_employee() + + self.assertEqual( + len(slip.benefit_line_ids), + 1, + "There should be only 1 benefit line attached to payslip", + ) + self.assertEqual( + slip.benefit_line_ids[0].earnings, + 500, + "Benefit line should show 500.00 earnings", + ) + self.assertEqual( + slip.benefit_line_ids[0].deductions, + 0, + "Benefit line should show NO deductions", + ) + + def test_get_benefit_policy_partial(self): + + start = date(2021, 1, 1) + end = date(2021, 1, 31) + policy_end = date(2021, 1, 15) + self.benefit_create_vals.update({"link2payroll": True}) + benefit = self.create_benefit(self.benefit_create_vals) + self.create_premium(benefit, start=start, amount=100.00) + self.create_policy(self.eeJohn, benefit, start, policy_end) + self.create_contract(self.eeJohn.id, "draft", "done", start).signal_confirm() + slip = self.Payslip.create( + { + "employee_id": self.eeJohn.id, + "date_from": start, + "date_to": end, + } + ) + slip.onchange_employee() + + self.assertEqual( + fields.Float.compare(slip.benefit_line_ids[0].ppf, 0.5, precision_digits=1), + 0, + "The benefit ppf should be 0.5 because the policy lasted 1/2 month", + ) + + def test_get_benefit_policy_allowance(self): + + start = date(2021, 1, 1) + end = date(2021, 1, 31) + self.benefit_create_vals.update({"link2payroll": True}) + benefit = self.create_benefit(self.benefit_create_vals) + self.create_earning(benefit, start=start, allowance=100.00) + self.create_policy(self.eeJohn, benefit, start) + self.create_contract(self.eeJohn.id, "draft", "done", start).signal_confirm() + slip = self.Payslip.create( + { + "employee_id": self.eeJohn.id, + "date_from": start, + "date_to": end, + } + ) + slip.onchange_employee() + + self.assertEqual( + len(slip.benefit_line_ids), + 1, + "There should be only 1 benefit line attached to payslip", + ) + self.assertEqual( + slip.benefit_line_ids[0].earnings, + 100, + "Benefit should indicate allowance of 100", + ) + self.assertEqual( + slip.benefit_line_ids[0].deductions, + 0, + "Benefit line should show no dedections", + ) + + def test_benefit_policy_ppf(self): + """ + Test for off-by-one error when calculating policy and payroll days. + 29 / 30 as opposed to 30 / 31. Manifests as ppf calculation error. + """ + + start = date(2021, 1, 1) + end = date(2021, 1, 31) + self.benefit_create_vals.update({"link2payroll": True}) + benefit = self.create_benefit(self.benefit_create_vals) + self.create_earning(benefit, start=start, allowance=100.00) + self.create_policy(self.eeJohn, benefit, start + timedelta(days=1)) + self.create_contract(self.eeJohn.id, "draft", "done", start).signal_confirm() + slip = self.Payslip.create( + { + "employee_id": self.eeJohn.id, + "date_from": start, + "date_to": end, + } + ) + slip.onchange_employee() + + self.assertEqual( + len(slip.benefit_line_ids), + 1, + "There should be only 1 benefit line attached to payslip", + ) + self.assertEqual( + round(slip.benefit_line_ids[0].ppf, 4), + 0.9677, + "Benefit should indicate PPF of 0.9677", + ) + self.assertEqual( + benefit.code, + slip.benefit_line_ids[0].code, + "The benefit appears in the payslip's benefit lines", + ) + + def test_onchange_deletes_old_values(self): + + start = date(2021, 1, 1) + end = date(2021, 1, 31) + self.benefit_create_vals.update({"link2payroll": True}) + benefit = self.create_benefit(self.benefit_create_vals) + self.create_earning(benefit, start=start, allowance=100.00) + self.create_policy(self.eeJohn, benefit, start) + self.create_contract(self.eeJohn.id, "draft", "done", start).signal_confirm() + slip = self.Payslip.create( + { + "employee_id": self.eeJohn.id, + "date_from": start, + "date_to": end, + } + ) + slip.onchange_employee() + slip.onchange_employee() + + self.assertEqual( + len(slip.benefit_line_ids), + 1, + "There should be only 1 benefit line attached to payslip", + ) + + def test_no_employee_no_contract(self): + + frm = Form(self.Payslip) + frm.date_from = date(2021, 1, 1) + frm.date_to = date(2021, 1, 31) + self.assertTrue( + True, "If we've reached this far without an exception thrown we're good" + ) + + def test_benefit_policy_premium_refund(self): + + start = date(2021, 1, 1) + end = date(2021, 1, 31) + self.benefit_create_vals.update({"link2payroll": True}) + bn = self.create_benefit(self.benefit_create_vals) + self.create_premium( + bn, + start, + ptype="monthly", + amount=100, + total=300, + ) + pol = self.create_policy(self.eeJohn, bn, start) + pol.state_open() + self.create_contract(self.eeJohn.id, "draft", "done", start).signal_confirm() + slip = self.Payslip.create( + { + "employee_id": self.eeJohn.id, + "date_from": start, + "date_to": end, + } + ) + slip.onchange_employee() + self.assertEqual( + bn.code, + slip.benefit_line_ids[0].code, + "The benefit appears in the payslip's benefit lines", + ) + slip.compute_sheet() + slip.action_payslip_done() + self.assertEqual( + len(slip.premium_payment_ids), 1, "There is one premium payment" + ) + self.assertEqual( + slip.premium_payment_ids[0].state, + "done", + "The premium payment is done", + ) + + slip.refund_sheet() + + self.assertEqual( + slip.premium_payment_ids[0].state, + "cancel", + "The premium payment has been cancelled", + ) diff --git a/hr_benefit_payroll/views/benefit_policy_view.xml b/hr_benefit_payroll/views/benefit_policy_view.xml new file mode 100644 index 00000000..d30f167f --- /dev/null +++ b/hr_benefit_payroll/views/benefit_policy_view.xml @@ -0,0 +1,23 @@ + + + + + + + + hr.benefit.policy.form.inherit.payroll + hr.benefit.policy + + + + + + + + + + + + + + diff --git a/hr_benefit_payroll/views/benefit_premium_payment_view.xml b/hr_benefit_payroll/views/benefit_premium_payment_view.xml new file mode 100644 index 00000000..844b67ea --- /dev/null +++ b/hr_benefit_payroll/views/benefit_premium_payment_view.xml @@ -0,0 +1,124 @@ + + + + + + + + hr.benefit.premium.payment.filter + hr.benefit.premium.payment + + + + + + + + + + + + + + + + + + + + hr.benefit.premium.payment.tree + hr.benefit.premium.payment + + + + + + + + + + + + + hr.benefit.premium.payment.from + hr.benefit.premium.payment + +
+
+
+ + + + + + + + + + + +
+
+
+ + + Premium Payments + hr.benefit.premium.payment + tree,form + + + + +
+
diff --git a/hr_benefit_payroll/views/benefit_view.xml b/hr_benefit_payroll/views/benefit_view.xml new file mode 100644 index 00000000..c4fa24b5 --- /dev/null +++ b/hr_benefit_payroll/views/benefit_view.xml @@ -0,0 +1,29 @@ + + + + + hr.benefit.tree.payroll + hr.benefit + + 99 + + + + + + + + + hr.benefit.form.payroll + hr.benefit + + 99 + + + + + + + + + diff --git a/hr_benefit_payroll/views/hr_employee_view.xml b/hr_benefit_payroll/views/hr_employee_view.xml new file mode 100644 index 00000000..08359490 --- /dev/null +++ b/hr_benefit_payroll/views/hr_employee_view.xml @@ -0,0 +1,31 @@ + + + + + + hr.employee.view.inherit.benefits + hr.employee + 99 + + + + hr.group_hr_user,payroll.group_payroll_user + + + hr.group_hr_user,payroll.group_payroll_user + + + + + + diff --git a/hr_benefit_payroll/views/hr_payslip_view.xml b/hr_benefit_payroll/views/hr_payslip_view.xml new file mode 100644 index 00000000..af7ff2f0 --- /dev/null +++ b/hr_benefit_payroll/views/hr_payslip_view.xml @@ -0,0 +1,52 @@ + + + + + + + hr.payslip.form + hr.payslip + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hr_benefit_payroll/views/hr_salary_rule_view.xml b/hr_benefit_payroll/views/hr_salary_rule_view.xml new file mode 100644 index 00000000..cdd3c306 --- /dev/null +++ b/hr_benefit_payroll/views/hr_salary_rule_view.xml @@ -0,0 +1,21 @@ + + + + + + + hr.salary.rule.form.inherit + hr.salary.rule + + + + + + + + + + + + + diff --git a/hr_contract_status/README.rst b/hr_contract_status/README.rst new file mode 100644 index 00000000..cd35a97a --- /dev/null +++ b/hr_contract_status/README.rst @@ -0,0 +1,72 @@ +=========================== +Stateful Employee Contracts +=========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:ab0d674ed4edb1c0395cafc17f6f418d6387e4e97fea2e2e40a5ac16d230eee0 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-trevi--software%2Ftrevi--hr-lightgray.png?logo=github + :target: https://github.com/trevi-software/trevi-hr/tree/14.0/hr_contract_status + :alt: trevi-software/trevi-hr + +|badge1| |badge2| |badge3| + +This module implements employee contract workflow and notifications. +* Mangae an employee contract life-cycle +* Get notified when employees near the end of their trial period +* Get notified when an employee's contract is about to expire + +**Table of contents** + +.. contents:: + :local: + +Changelog +========= + +14.0.1.0.1 (2021-10-07) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [FIX] Scheduled actions related to contract state no longer fail because of a programming error + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* TREVI Software +* Michael Telahun Makonnen + +Other credits +~~~~~~~~~~~~~ + +* Michael Telahun Makonnen + +Maintainers +~~~~~~~~~~~ + +This module is part of the `trevi-software/trevi-hr `_ project on GitHub. + +You are welcome to contribute. diff --git a/hr_contract_status/__init__.py b/hr_contract_status/__init__.py new file mode 100644 index 00000000..2b0f2dca --- /dev/null +++ b/hr_contract_status/__init__.py @@ -0,0 +1,5 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import models diff --git a/hr_contract_status/__manifest__.py b/hr_contract_status/__manifest__.py new file mode 100644 index 00000000..7efea25a --- /dev/null +++ b/hr_contract_status/__manifest__.py @@ -0,0 +1,27 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + "name": "Stateful Employee Contracts", + "summary": "Workflows and notifications on employee contracts.", + "version": "15.0.1.0.0", + "category": "Human Resources", + "license": "AGPL-3", + "author": "TREVI Software, Michael Telahun Makonnen", + "images": ["static/src/img/main_screenshot.png"], + "website": "https://github.com/trevi-software/trevi-hr", + "depends": [ + "hr", + "hr_contract", + "hr_contract_values", + "trevi_hr_usability", + ], + "data": [ + "security/ir.model.access.csv", + "data/hr_contract_data.xml", + "views/hr_contract_view.xml", + "views/res_config_view.xml", + ], + "installable": True, +} diff --git a/hr_contract_status/data/hr_contract_data.xml b/hr_contract_status/data/hr_contract_data.xml new file mode 100644 index 00000000..f1dcca48 --- /dev/null +++ b/hr_contract_status/data/hr_contract_data.xml @@ -0,0 +1,22 @@ + + + + + + + + Trial Period Ending + hr.contract + + Trial Period is ending + + + + Contract Period Ending + hr.contract + + Contract Period is ending + + + + diff --git a/hr_contract_status/i18n/hr_contract_status.pot b/hr_contract_status/i18n/hr_contract_status.pot new file mode 100644 index 00000000..ab7993ec --- /dev/null +++ b/hr_contract_status/i18n/hr_contract_status.pot @@ -0,0 +1,232 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_contract_status +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: hr_contract_status +#: model:ir.model.fields,field_description:hr_contract_status.field_res_config_settings__concurrent_contracts +msgid "Allow concurrent contracts" +msgstr "" + +#. module: hr_contract_status +#: model:ir.model.fields,help:hr_contract_status.field_res_config_settings__concurrent_contracts +msgid "Allow multiple concurrent contracts for an employee" +msgstr "" + +#. module: hr_contract_status +#: model_terms:ir.ui.view,arch_db:hr_contract_status.view_hr_config_contract_state +msgid "Allow multiple concurrent employee contracts." +msgstr "" + +#. module: hr_contract_status +#: model:ir.model,name:hr_contract_status.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: hr_contract_status +#: model_terms:ir.ui.view,arch_db:hr_contract_status.view_contract_form +msgid "Confirm" +msgstr "" + +#. module: hr_contract_status +#: model:mail.message.subtype,name:hr_contract_status.mt_alert_contract_ending +msgid "Contract Period Ending" +msgstr "" + +#. module: hr_contract_status +#: model:mail.message.subtype,description:hr_contract_status.mt_alert_contract_ending +msgid "Contract Period is ending" +msgstr "" + +#. module: hr_contract_status +#: model:ir.ui.menu,name:hr_contract_status.submenu_hr_contracts +msgid "Contracts" +msgstr "" + +#. module: hr_contract_status +#: model:ir.actions.act_window,name:hr_contract_status.open_draft_contracts +#: model:ir.ui.menu,name:hr_contract_status.menu_draft_contracts +#: model_terms:ir.ui.view,arch_db:hr_contract_status.view_draft_contracts_tree +msgid "Contracts to be Approved" +msgstr "" + +#. module: hr_contract_status +#: model:ir.model.fields,field_description:hr_contract_status.field_hr_contract__department_id +#: model:ir.model.fields,field_description:hr_contract_status.field_hr_employee__department_id +#: model:ir.model.fields,field_description:hr_contract_status.field_hr_job__department_id +msgid "Department" +msgstr "" + +#. module: hr_contract_status +#: model:ir.model.fields,field_description:hr_contract_status.field_hr_contract__display_name +#: model:ir.model.fields,field_description:hr_contract_status.field_hr_employee__display_name +#: model:ir.model.fields,field_description:hr_contract_status.field_hr_job__display_name +#: model:ir.model.fields,field_description:hr_contract_status.field_res_config_settings__display_name +msgid "Display Name" +msgstr "" + +#. module: hr_contract_status +#: model:ir.model.fields,field_description:hr_contract_status.field_hr_contract__date_end_effective +msgid "Effective End Date" +msgstr "" + +#. module: hr_contract_status +#: model:ir.model,name:hr_contract_status.model_hr_employee +#: model:ir.model.fields,field_description:hr_contract_status.field_hr_contract__employee_id +msgid "Employee" +msgstr "" + +#. module: hr_contract_status +#: model:ir.model,name:hr_contract_status.model_hr_contract +msgid "Employee Contract" +msgstr "" + +#. module: hr_contract_status +#: model_terms:ir.ui.view,arch_db:hr_contract_status.view_hr_config_contract_state +msgid "Employee Contracts" +msgstr "" + +#. module: hr_contract_status +#: model:ir.model.fields,help:hr_contract_status.field_hr_contract__wage +msgid "Employee's monthly gross wage." +msgstr "" + +#. module: hr_contract_status +#: model_terms:ir.ui.view,arch_db:hr_contract_status.view_hr_config_contract_state +msgid "Employees can have more than one contract active at a time." +msgstr "" + +#. module: hr_contract_status +#: model_terms:ir.ui.view,arch_db:hr_contract_status.view_contract_form +msgid "End Contract" +msgstr "" + +#. module: hr_contract_status +#: model:ir.actions.act_window,name:hr_contract_status.open_expiring_contracts +#: model:ir.ui.menu,name:hr_contract_status.menu_expiring_contracts +msgid "Ending Trials & Contracts" +msgstr "" + +#. module: hr_contract_status +#: model_terms:ir.ui.view,arch_db:hr_contract_status.view_expiring_contracts_tree +msgid "Expiring Contracts" +msgstr "" + +#. module: hr_contract_status +#: model:ir.model.fields,field_description:hr_contract_status.field_hr_contract__id +#: model:ir.model.fields,field_description:hr_contract_status.field_hr_employee__id +#: model:ir.model.fields,field_description:hr_contract_status.field_hr_job__id +#: model:ir.model.fields,field_description:hr_contract_status.field_res_config_settings__id +msgid "ID" +msgstr "" + +#. module: hr_contract_status +#: model:ir.model.fields,field_description:hr_contract_status.field_hr_employee__job_id +msgid "Job" +msgstr "" + +#. module: hr_contract_status +#: model:ir.model,name:hr_contract_status.model_hr_job +#: model:ir.model.fields,field_description:hr_contract_status.field_hr_contract__job_id +msgid "Job Position" +msgstr "" + +#. module: hr_contract_status +#: model:ir.model.fields,field_description:hr_contract_status.field_hr_contract__end_job_id +msgid "Job Title" +msgstr "" + +#. module: hr_contract_status +#: model:ir.model.fields,field_description:hr_contract_status.field_hr_contract____last_update +#: model:ir.model.fields,field_description:hr_contract_status.field_hr_employee____last_update +#: model:ir.model.fields,field_description:hr_contract_status.field_hr_job____last_update +#: model:ir.model.fields,field_description:hr_contract_status.field_res_config_settings____last_update +msgid "Last Modified on" +msgstr "" + +#. module: hr_contract_status +#: model:ir.model.fields,field_description:hr_contract_status.field_hr_contract__date_end_original +msgid "Original End Date" +msgstr "" + +#. module: hr_contract_status +#: model:ir.model.fields,field_description:hr_contract_status.field_hr_contract__structure_type_id +msgid "Salary Structure Type" +msgstr "" + +#. module: hr_contract_status +#: model:ir.model.fields,field_description:hr_contract_status.field_hr_contract__date_start +msgid "Start Date" +msgstr "" + +#. module: hr_contract_status +#: model:ir.model.fields,help:hr_contract_status.field_hr_contract__date_start +msgid "Start date of the contract." +msgstr "" + +#. module: hr_contract_status +#: model:ir.model.fields,field_description:hr_contract_status.field_hr_contract__state_ending +msgid "State Ending" +msgstr "" + +#. module: hr_contract_status +#: model:ir.model.fields,field_description:hr_contract_status.field_hr_contract__state +msgid "Status" +msgstr "" + +#. module: hr_contract_status +#: model:ir.model.fields,help:hr_contract_status.field_hr_contract__state +msgid "Status of the contract" +msgstr "" + +#. module: hr_contract_status +#: code:addons/hr_contract_status/models/hr_contract.py:0 +#, python-format +msgid "The trial period of %s is about to end." +msgstr "" + +#. module: hr_contract_status +#: model_terms:ir.actions.act_window,help:hr_contract_status.open_expiring_contracts +msgid "" +"There are currently no contracts or trial periods that are about to expire." +msgstr "" + +#. module: hr_contract_status +#: model_terms:ir.actions.act_window,help:hr_contract_status.open_draft_contracts +msgid "There are currently no contracts that need to be approved." +msgstr "" + +#. module: hr_contract_status +#: model:ir.model.fields.selection,name:hr_contract_status.selection__hr_contract__state__trial +msgid "Trial" +msgstr "" + +#. module: hr_contract_status +#: model:ir.model.fields,field_description:hr_contract_status.field_hr_contract__trial_ending +msgid "Trial Ending" +msgstr "" + +#. module: hr_contract_status +#: model:mail.message.subtype,name:hr_contract_status.mt_alert_trial_ending +msgid "Trial Period Ending" +msgstr "" + +#. module: hr_contract_status +#: model:mail.message.subtype,description:hr_contract_status.mt_alert_trial_ending +msgid "Trial Period is ending" +msgstr "" + +#. module: hr_contract_status +#: model:ir.model.fields,field_description:hr_contract_status.field_hr_contract__wage +msgid "Wage" +msgstr "" diff --git a/hr_contract_status/models/__init__.py b/hr_contract_status/models/__init__.py new file mode 100644 index 00000000..524f729f --- /dev/null +++ b/hr_contract_status/models/__init__.py @@ -0,0 +1,8 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import hr_contract +from . import hr_employee +from . import hr_job +from . import res_config diff --git a/hr_contract_status/models/hr_contract.py b/hr_contract_status/models/hr_contract.py new file mode 100644 index 00000000..92db04bb --- /dev/null +++ b/hr_contract_status/models/hr_contract.py @@ -0,0 +1,245 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + + +from datetime import date + +from dateutil.relativedelta import relativedelta + +from odoo import _, api, fields, models + + +class HrContract(models.Model): + + _name = "hr.contract" + _inherit = "hr.contract" + + trial_ending = fields.Boolean() + state_ending = fields.Boolean() + date_end_effective = fields.Date(string="Effective End Date", readonly=True) + date_end_original = fields.Date(string="Original End Date", readonly=True) + state = fields.Selection( + selection_add=[ + ("draft",), + ("trial", "Trial"), + ("open",), + ("close",), + ("cancel",), + ], + ondelete={"trial": "set null"}, + default="draft", + readonly=True, + ) + + department_id = fields.Many2one( + compute="_compute_department", + store=True, + readonly=True, + ) + + # At contract end this field will hold the job_id, and the + # job_id field will be set to null so that modules that + # reference job_id don't include deactivated employees. + # XXX ToDo: is it possible to change those references rather than using this hack? + end_job_id = fields.Many2one( + comodel_name="hr.job", string="Job Title", readonly=True + ) + + # The following are redefined again to make them editable only in certain states + employee_id = fields.Many2one( + readonly=True, states={"draft": [("readonly", False)]} + ) + structure_type_id = fields.Many2one( + readonly=True, states={"draft": [("readonly", False)]} + ) + job_id = fields.Many2one( + comodel_name="hr.job", + compute=False, + domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", + required=False, + states={ + "draft": [("required", True)], + "trial": [("required", True)], + "open": [("required", True)], + }, + tracking=True, + ) + date_start = fields.Date( + readonly=True, + states={"draft": [("readonly", False)]}, + ) + wage = fields.Monetary( + readonly=True, + states={ + "draft": [("readonly", False)], + "trial": [("readonly", False)], + "trial_ending": [("readonly", False)], + }, + ) + + @api.depends("job_id") + def _compute_department(self): + for contract in self: + contract.department_id = contract.job_id.department_id + + # Override from inherited model. job_id and department_id in hr.employee should be + # calculated from the contract. + # + @api.depends("employee_id") + def _compute_employee_contract(self): + for contract in self.filtered("employee_id"): + contract.resource_calendar_id = contract.employee_id.resource_calendar_id + contract.company_id = contract.employee_id.company_id + + @api.constrains("employee_id", "state", "kanban_state", "date_start", "date_end") + def _check_current_contract(self): + + allow = ( + self.env["ir.config_parameter"] + .sudo() + .get_param("hr_contract_status.concurrent_contracts", False) + ) + if allow: + return + return super(HrContract, self)._check_current_contract() + + def _track_subtype(self, init_values): + self.ensure_one() + if "state" in init_values: + if self.state == "trial_ending": + return self.env.ref("hr_contract_status.mt_alert_trial_ending") + elif self.state == "contract_ending": + return self.env.ref("hr_contract_status.mt_alert_contract_ending") + return super(HrContract, self)._track_subtype(init_values) + + @api.model + def update_state(self): + + # New contract with trial period + self.search( + [ + ("state", "=", "draft"), + ("kanban_state", "=", "done"), + ("date_start", "<=", fields.Date.to_string(date.today())), + ("trial_date_end", ">=", fields.Date.to_string(date.today())), + ] + ).write({"state": "trial"}) + + # Trial period is ending + contracts = self.search( + [ + ("state", "=", "trial"), + ( + "trial_date_end", + "<=", + fields.Date.to_string(date.today() + relativedelta(days=7)), + ), + ] + ) + for contract in contracts: + contract.kanban_state = "blocked" + contract.trial_ending = True + contract.activity_schedule( + "mail.mail_activity_data_todo", + contract.trial_date_end, + _("The trial period of %s is about to end.", contract.employee_id.name), + user_id=contract.hr_responsible_id.id or self.env.uid, + ) + + # Trial period has ended + contracts = self.search( + [ + ("state", "=", "trial"), + ( + "trial_date_end", + "<=", + fields.Date.to_string(date.today() - relativedelta(days=1)), + ), + ] + ) + for contract in contracts: + contract.state = "open" + contract.kanban_state = "normal" + contract.trial_ending = False + + # Contract is expiring + self.search( + [ + ("state", "=", "open"), + ("kanban_state", "!=", "blocked"), + "|", + "&", + ( + "date_end", + "<=", + fields.Date.to_string(date.today() + relativedelta(days=7)), + ), + ( + "date_end", + ">=", + fields.Date.to_string(date.today() + relativedelta(days=1)), + ), + "&", + ( + "visa_expire", + "<=", + fields.Date.to_string(date.today() + relativedelta(days=60)), + ), + ( + "visa_expire", + ">=", + fields.Date.to_string(date.today() + relativedelta(days=1)), + ), + ] + ).write({"state_ending": True}) + + # Contract has expired + self.search( + [ + ("state", "=", "open"), + "|", + ( + "date_end", + "<=", + fields.Date.to_string(date.today() + relativedelta(days=1)), + ), + ( + "visa_expire", + "<=", + fields.Date.to_string(date.today() + relativedelta(days=1)), + ), + ] + ).write({"state_ending": False}) + + return super(HrContract, self).update_state() + + def condition_trial_period(self): + self.ensure_one() + dToday = fields.Date.today() + if not self.trial_date_end or ( + self.trial_date_end and self.trial_date_end < dToday + ): + return False + return True + + def signal_confirm(self): + for rec in self: + if rec.kanban_state == "done": + rec.kanban_state = "normal" + if rec.condition_trial_period(): + rec.state = "trial" + else: + rec.state = "open" + + def signal_close(self): + for c in self: + vals = {"state": "close"} + if not c.date_end or c.date_end >= date.today(): + vals.update({"date_end": date.today()}) + c.write(vals) + + def write(self, vals): + if vals.get("state") == "trial": + self._assign_open_contract() + return super(HrContract, self).write(vals) diff --git a/hr_contract_status/models/hr_employee.py b/hr_contract_status/models/hr_employee.py new file mode 100644 index 00000000..63b1ad00 --- /dev/null +++ b/hr_contract_status/models/hr_employee.py @@ -0,0 +1,43 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + + +from odoo import api, fields, models + + +class HrEmployee(models.Model): + _inherit = "hr.employee" + + department_id = fields.Many2one(compute="_compute_contract", store=True) + job_id = fields.Many2one(compute="_compute_contract", store=True) + + @api.depends("contract_id", "contract_id.job_id") + def _compute_contract(self): + for employee in self.filtered(lambda c: c.contract_id): + employee.job_id = employee.contract_id.job_id + employee.department_id = employee.contract_id.department_id + + @api.depends("contract_id", "contract_id.state", "contract_id.kanban_state") + def _compute_contract_warning(self): + for employee in self: + employee.contract_warning = ( + not employee.contract_id + or employee.contract_id.kanban_state == "blocked" + or employee.contract_id.state not in ["open", "trial"] + ) + + def _get_contracts( + self, date_from, date_to, states=["open"], kanban_state=False # noqa: B006 + ): # pylint: disable=W0102 + + # Over-ride base class method to includes Closed/Ended contracts. Useful + # when multiple consecutive contracts occur in a payroll period. + # + default_states = ["open"] + if states == default_states: + states = ["trial", "open", "close"] + + return super(HrEmployee, self)._get_contracts( + date_from, date_to, states=states, kanban_state=False + ) diff --git a/hr_contract_status/models/hr_job.py b/hr_contract_status/models/hr_job.py new file mode 100644 index 00000000..75ca3a2f --- /dev/null +++ b/hr_contract_status/models/hr_job.py @@ -0,0 +1,12 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + + +from odoo import fields, models + + +class Job(models.Model): + _inherit = "hr.job" + + department_id = fields.Many2one(required=True) diff --git a/hr_contract_status/models/res_config.py b/hr_contract_status/models/res_config.py new file mode 100644 index 00000000..92c0e79e --- /dev/null +++ b/hr_contract_status/models/res_config.py @@ -0,0 +1,18 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + + +from odoo import fields, models + + +class ResConfig(models.TransientModel): + + _inherit = "res.config.settings" + + concurrent_contracts = fields.Boolean( + string="Allow concurrent contracts", + config_parameter="hr_contract_status.concurrent_contracts", + default=False, + help="Allow multiple concurrent contracts for an employee", + ) diff --git a/hr_contract_status/readme/CREDITS.rst b/hr_contract_status/readme/CREDITS.rst new file mode 100644 index 00000000..d264bc7e --- /dev/null +++ b/hr_contract_status/readme/CREDITS.rst @@ -0,0 +1 @@ +* Michael Telahun Makonnen diff --git a/hr_contract_status/readme/DESCRIPTION.rst b/hr_contract_status/readme/DESCRIPTION.rst new file mode 100644 index 00000000..4db4f63c --- /dev/null +++ b/hr_contract_status/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +This module implements employee contract workflow and notifications. +* Mangae an employee contract life-cycle +* Get notified when employees near the end of their trial period +* Get notified when an employee's contract is about to expire diff --git a/hr_contract_status/readme/HISTORY.rst b/hr_contract_status/readme/HISTORY.rst new file mode 100644 index 00000000..6709f831 --- /dev/null +++ b/hr_contract_status/readme/HISTORY.rst @@ -0,0 +1,4 @@ +14.0.1.0.1 (2021-10-07) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [FIX] Scheduled actions related to contract state no longer fail because of a programming error diff --git a/hr_contract_status/security/ir.model.access.csv b/hr_contract_status/security/ir.model.access.csv new file mode 100644 index 00000000..5fbae892 --- /dev/null +++ b/hr_contract_status/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_hr_contract_user,access_hr_contract,model_hr_contract,hr.group_hr_user,1,0,0,0 diff --git a/hr_contract_status/static/description/icon.png b/hr_contract_status/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/hr_contract_status/static/description/icon.png differ diff --git a/hr_contract_status/static/description/index.html b/hr_contract_status/static/description/index.html new file mode 100644 index 00000000..77d285b1 --- /dev/null +++ b/hr_contract_status/static/description/index.html @@ -0,0 +1,433 @@ + + + + + + +Stateful Employee Contracts + + + +
+

Stateful Employee Contracts

+ + +

Beta License: AGPL-3 trevi-software/trevi-hr

+

This module implements employee contract workflow and notifications. +* Mangae an employee contract life-cycle +* Get notified when employees near the end of their trial period +* Get notified when an employee’s contract is about to expire

+

Table of contents

+ +
+

Changelog

+
+

14.0.1.0.1 (2021-10-07)

+
    +
  • [FIX] Scheduled actions related to contract state no longer fail because of a programming error
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • TREVI Software
  • +
  • Michael Telahun Makonnen
  • +
+
+
+

Other credits

+ +
+
+

Maintainers

+

This module is part of the trevi-software/trevi-hr project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/hr_contract_status/static/src/img/main_screenshot.png b/hr_contract_status/static/src/img/main_screenshot.png new file mode 100644 index 00000000..ab78e067 Binary files /dev/null and b/hr_contract_status/static/src/img/main_screenshot.png differ diff --git a/hr_contract_status/tests/__init__.py b/hr_contract_status/tests/__init__.py new file mode 100644 index 00000000..042098a7 --- /dev/null +++ b/hr_contract_status/tests/__init__.py @@ -0,0 +1,8 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + + +from . import test_hr_contract +from . import test_hr_employee +from . import test_res_config_settings diff --git a/hr_contract_status/tests/test_hr_contract.py b/hr_contract_status/tests/test_hr_contract.py new file mode 100644 index 00000000..34c608e0 --- /dev/null +++ b/hr_contract_status/tests/test_hr_contract.py @@ -0,0 +1,225 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + + +from datetime import datetime + +from dateutil.relativedelta import relativedelta + +from odoo.tests import common + + +class TestContract(common.SavepointCase): + @classmethod + def setUpClass(cls): + super(TestContract, cls).setUpClass() + + cls.HrEmployee = cls.env["hr.employee"] + cls.HrContract = cls.env["hr.contract"] + cls.employee = cls.HrEmployee.create({"name": "John"}) + cls.test_contract = dict( + name="Test", wage=1, employee_id=cls.employee.id, state="draft" + ) + + def create_contract(self, state, kanban_state, start, end=None, trial_end=None): + return self.env["hr.contract"].create( + { + "name": "Contract", + "employee_id": self.employee.id, + "state": state, + "kanban_state": kanban_state, + "wage": 1, + "date_start": start, + "trial_date_end": trial_end, + "date_end": end, + } + ) + + def apply_cron(self): + self.env.ref( + "hr_contract.ir_cron_data_contract_update_state" + ).method_direct_trigger() + + def test_first_contract_open_notrial(self): + """The first contract when 'Running' goes to + 'open' state, if trial end isn't set.""" + + start = datetime.now().date() + c = self.create_contract("draft", "normal", start) + c.signal_confirm() + self.assertEqual("open", c.state) + self.assertEqual("normal", c.kanban_state) + + def test_first_contract_open_trial(self): + """The first contract when 'Running' goes to + trial period state, if trial end is set.""" + + start = datetime.now().date() + end = start + relativedelta(days=60) + c = self.create_contract("draft", "normal", start, trial_end=end) + c.signal_confirm() + self.assertEqual("trial", c.state) + self.assertEqual("normal", c.kanban_state) + + def test_next_contract_open_running(self): + """Subsequent contracts go directly to 'Running' state""" + + start = datetime.strptime("2015-11-01", "%Y-%m-%d").date() + end = datetime.strptime("2015-11-30", "%Y-%m-%d").date() + self.create_contract("close", "normal", start, end) + + start = datetime.strptime("2015-12-01", "%Y-%m-%d").date() + c2 = self.create_contract("draft", "normal", start) + c2.signal_confirm() + self.assertEqual("open", c2.state) + self.assertEqual("normal", c2.kanban_state) + + def test_start_trial(self): + """Cron update changes 'draft' contract into 'trial'""" + + self.test_contract.update( + dict( + trial_date_end=datetime.now() + relativedelta(days=100), + kanban_state="done", + ) + ) + contract = self.HrContract.create(self.test_contract) + self.apply_cron() + self.assertEqual("trial", contract.state) + self.assertEqual("normal", contract.kanban_state) + self.assertEqual(False, contract.trial_ending) + self.assertEqual(False, self.employee.contract_warning) + + def test_trial_ending(self): + self.test_contract.update( + dict(trial_date_end=datetime.now() + relativedelta(days=100)) + ) + contract = self.HrContract.create(self.test_contract) + contract.signal_confirm() + self.assertEqual("trial", contract.state) + self.assertEqual("normal", contract.kanban_state) + self.assertEqual(False, contract.trial_ending) + self.assertEqual(False, self.employee.contract_warning) + + self.test_contract.update( + dict(trial_date_end=datetime.now() + relativedelta(days=5)) + ) + contract.write(self.test_contract) + self.apply_cron() + self.assertEqual(contract.state, "trial") + self.assertEqual(contract.trial_ending, True) + self.assertEqual(contract.kanban_state, "blocked") + + def test_trial_ended(self): + self.test_contract.update( + { + "date_start": datetime.now() + relativedelta(days=-50), + "trial_date_end": datetime.now() + relativedelta(days=100), + } + ) + contract = self.HrContract.create(self.test_contract) + contract.signal_confirm() + self.assertEqual("trial", contract.state) + self.assertEqual("normal", contract.kanban_state) + self.assertEqual(False, contract.trial_ending) + self.assertEqual(False, self.employee.contract_warning) + + self.test_contract.update( + { + "trial_date_end": datetime.now() + relativedelta(days=-1), + } + ) + contract.write(self.test_contract) + self.apply_cron() + self.assertEqual("open", contract.state) + self.assertEqual(False, contract.trial_ending) + self.assertEqual("normal", contract.kanban_state) + + def test_contract_ending(self): + """When a contract nears completion the relevant boolean should be set""" + + start = datetime.now().date() + end = start + relativedelta(days=100) + c = self.create_contract("draft", "normal", start, end) + c.signal_confirm() + self.assertEqual("open", c.state) + self.assertEqual("normal", c.kanban_state) + self.assertFalse(c.state_ending) + + c.date_end = start + relativedelta(days=5) + self.apply_cron() + self.assertEqual("open", c.state) + self.assertEqual("blocked", c.kanban_state) + self.assertTrue(c.state_ending) + + def test_contract_ended(self): + """When a contract has ended the state should be 'close'""" + + # Start + start = datetime.now().date() + relativedelta(days=-100) + today = datetime.now().date() + end = today + relativedelta(days=100) + c = self.create_contract("draft", "normal", start, end) + c.signal_confirm() + self.assertEqual("open", c.state) + self.assertEqual("normal", c.kanban_state) + self.assertFalse(c.state_ending) + + # Ending + c.date_end = today + relativedelta(days=5) + self.apply_cron() + self.assertEqual("open", c.state) + self.assertEqual("blocked", c.kanban_state) + self.assertTrue(c.state_ending) + + # Ended + c.date_end = today + relativedelta(days=-1) + self.apply_cron() + self.assertEqual("close", c.state) + self.assertEqual("normal", c.kanban_state) + self.assertFalse(c.state_ending) + + def test_signal_close(self): + """Calling signal close immediately ends the contract""" + + # Start + start = datetime.now().date() + relativedelta(days=-100) + today = datetime.now().date() + end = today + relativedelta(days=100) + c = self.create_contract("draft", "normal", start, end) + c.signal_confirm() + self.assertEqual("open", c.state) + self.assertEqual("normal", c.kanban_state) + self.assertFalse(c.state_ending) + + c.signal_close() + + # Ended + self.assertEqual(today, c.date_end) + self.assertEqual("close", c.state) + self.assertEqual("normal", c.kanban_state) + self.assertFalse(c.state_ending) + + def test_signal_close_ended_contract(self): + """Calling signal_close() doesn't alter contract end date that is in the past""" + + # Start + start = datetime.now().date() + relativedelta(days=-100) + today = datetime.now().date() + end = today + relativedelta(days=100) + c = self.create_contract("draft", "normal", start, end) + c.signal_confirm() + self.assertEqual("open", c.state) + self.assertEqual("normal", c.kanban_state) + self.assertFalse(c.state_ending) + + prev_end = today - relativedelta(days=1) + c.date_end = prev_end + c.signal_close() + + # Ended + self.assertEqual(prev_end, c.date_end) + self.assertEqual("close", c.state) + self.assertEqual("normal", c.kanban_state) + self.assertFalse(c.state_ending) diff --git a/hr_contract_status/tests/test_hr_employee.py b/hr_contract_status/tests/test_hr_employee.py new file mode 100644 index 00000000..e58f9fa2 --- /dev/null +++ b/hr_contract_status/tests/test_hr_employee.py @@ -0,0 +1,109 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + + +from datetime import date, datetime + +from dateutil.relativedelta import relativedelta + +from odoo.tests import common + + +class TestContract(common.SavepointCase): + @classmethod + def setUpClass(cls): + super(TestContract, cls).setUpClass() + + cls.HrEmployee = cls.env["hr.employee"] + cls.HrContract = cls.env["hr.contract"] + cls.Job = cls.env["hr.job"] + cls.Dept = cls.env["hr.department"] + cls.employee = cls.HrEmployee.create({"name": "John"}) + cls.test_contract = dict( + name="Test", wage=1, employee_id=cls.employee.id, state="draft" + ) + cls.dept_rnd = cls.Dept.create({"name": "R&D"}) + cls.job_ux_designer = cls.Job.create( + { + "name": "#UX Designer", + "department_id": cls.dept_rnd.id, + } + ) + + def create_contract(self, start, end=None, trial_end=None, state="draft"): + return self.env["hr.contract"].create( + { + "name": "Contract", + "employee_id": self.employee.id, + "wage": 1, + "date_start": start, + "trial_date_end": trial_end, + "date_end": end, + "job_id": self.job_ux_designer.id, + "state": state, + } + ) + + def apply_cron(self): + self.env.ref( + "hr_contract.ir_cron_data_contract_update_state" + ).method_direct_trigger() + + def test_new_contract(self): + """Creation of employee contract sets department and job""" + + self.assertFalse(self.employee.job_id) + self.assertFalse(self.employee.department_id) + + start = datetime.now().date() + contract = self.create_contract(start) + contract.signal_confirm() + + self.assertEqual(self.job_ux_designer, self.employee.job_id) + self.assertEqual(self.dept_rnd, self.employee.department_id) + + def test_new_contract_sets_contract_id(self): + """Creation of employee contract sets link to contract""" + + self.assertFalse(self.employee.contract_id) + + start = date.today() + trial_end = date.today() + relativedelta(days=60) + contract = self.create_contract(start, trial_end=trial_end) + contract.signal_confirm() + + self.assertEqual(contract, self.employee.contract_id) + + def test_consecutive_contracts(self): + """List of contracts in period includes 'closed' contracts""" + + start = date(2021, 1, 1) + end = date(2021, 1, 14) + jan_end = date(2021, 1, 31) + start2 = start + relativedelta(days=14) + cc1 = self.create_contract(start, end=end, state="close") + cc2 = self.create_contract(start2) + cc2.signal_confirm() + contracts = self.employee._get_contracts(start, jan_end) + self.assertEqual(cc1.state, "close", "Contract 1 has ended") + + self.assertIn(cc1, contracts, "Contract 1 (closed) is in list of contracts") + self.assertIn(cc1, contracts, "Contract 2 (open) is in list of contracts") + + def test_get_trial_contract(self): + """_get_contracts() returns 'trial' contracts""" + + start = datetime.now().date() + contract = self.create_contract(start, trial_end=start + relativedelta(days=30)) + contract.signal_confirm() + + self.assertEqual(contract.state, "trial", "The contract is in 'trial' state") + + clist = self.employee._get_contracts( + date_from=start, date_to=start + relativedelta(days=30) + ) + + self.assertIn( + contract, clist, "Trial contract is returned in list of contracts" + ) diff --git a/hr_contract_status/tests/test_res_config_settings.py b/hr_contract_status/tests/test_res_config_settings.py new file mode 100644 index 00000000..aafb1d06 --- /dev/null +++ b/hr_contract_status/tests/test_res_config_settings.py @@ -0,0 +1,73 @@ +# Copyright (C) 2021 Trevi Software (https://trevi.et) +# Copyright (C) 2013 Michael Telahun Makonnen . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + + +from datetime import datetime + +from odoo.exceptions import ValidationError +from odoo.tests import common + + +class TestContract(common.SavepointCase): + @classmethod + def setUpClass(cls): + super(TestContract, cls).setUpClass() + + cls.IrConfig = cls.env["ir.config_parameter"] + cls.HrEmployee = cls.env["hr.employee"] + cls.HrContract = cls.env["hr.contract"] + cls.employee = cls.HrEmployee.create({"name": "John"}) + + def create_contract(self, state, kanban_state, start, end=None): + return self.env["hr.contract"].create( + { + "name": "Contract", + "employee_id": self.employee.id, + "state": state, + "kanban_state": kanban_state, + "wage": 1, + "date_start": start, + "date_end": end, + } + ) + + def test_no_concurrent_contracts(self): + """If concurrent contracts aren't enabled creating a + second contract fails""" + + self.assertFalse( + self.IrConfig.get_param("hr_contract_status.concurrent_contracts", False) + ) + start = datetime.strptime("2015-11-01", "%Y-%m-%d").date() + end = datetime.strptime("2015-11-30", "%Y-%m-%d").date() + self.create_contract("open", "normal", start, end) + + # Incoming contract + with self.assertRaises( + ValidationError, + msg="It should not create two contract in state open or incoming", + ): + start = datetime.strptime("2015-11-15", "%Y-%m-%d").date() + end = datetime.strptime("2015-12-30", "%Y-%m-%d").date() + self.create_contract("draft", "done", start, end) + + def test_concurrent_contracts(self): + """If concurrent contracts are enabled creating more than + one open contract suceeds""" + + self.IrConfig.sudo().set_param("hr_contract_status.concurrent_contracts", True) + self.assertTrue( + self.IrConfig.get_param("hr_contract_status.concurrent_contracts", False) + ) + start = datetime.strptime("2015-11-01", "%Y-%m-%d").date() + end = datetime.strptime("2015-11-30", "%Y-%m-%d").date() + self.create_contract("open", "normal", start, end) + + # Incoming contract + try: + start = datetime.strptime("2015-11-15", "%Y-%m-%d").date() + end = datetime.strptime("2015-12-30", "%Y-%m-%d").date() + self.create_contract("draft", "done", start, end) + except ValidationError: + self.fail("Unexpected ValidationError exception") diff --git a/hr_contract_status/views/hr_contract_view.xml b/hr_contract_status/views/hr_contract_view.xml new file mode 100644 index 00000000..e1cfa09d --- /dev/null +++ b/hr_contract_status/views/hr_contract_view.xml @@ -0,0 +1,187 @@ + + + + + + + + + + + + + hr.contract.tree.state + hr.contract + + + + + + + + + + + + + + + hr.contract.contract.expire.tree + hr.contract + + + + + + + + + + + + + Ending Trials & Contracts + hr.contract + tree,form + + [('state','in',['trial', 'open']),'|', ('trial_ending', '=', True),('state_ending','=',True)] + +

+ There are currently no contracts or trial periods that are about to expire. +

+
+
+ + + + hr.contract.contract.draft.tree + hr.contract + + + + + + + + + + + + + + Contracts to be Approved + hr.contract + tree,form + + [('state','in',['draft'])] + +

+ There are currently no contracts that need to be approved. +

+
+
+ + + + + hr.contract.form.inherit.state + hr.contract + + + + + + | + | + + + + + diff --git a/hr_photobooth/views/assets.xml b/hr_photobooth/views/assets.xml new file mode 100644 index 00000000..98a1a61c --- /dev/null +++ b/hr_photobooth/views/assets.xml @@ -0,0 +1,15 @@ + + +