diff options
| author | Lucas Lefèvre <lul@odoo.com> | 2019-03-29 12:03:11 +0100 |
|---|---|---|
| committer | Yannick Tivisse <yti@odoo.com> | 2019-06-13 10:02:10 +0200 |
| commit | 22437e331a60c81a80d619c075a9d516e980c078 (patch) | |
| tree | 9178688c381961b55be5329fbc443b42fb3a0c13 | |
| parent | 55e7b702bb7f01c5f3bd3efbccf82c2d2c661f49 (diff) | |
[IMP] hr_contract: Sync employee and contract calendars mismatch
Purpose
=======
An employee and its current contract should have the same
calendar. Currently, if the contract's calendar is changed,
the employee's calendar is not changed.
In the same way, if a contract is created directly in `open`
state, the employee's calendar is not changed.
Finally, the calendar synchronization when the employee
contract is changed is only done in payroll, for no reason.
Specification
=============
Fix the three issues described above.
If the employee's calendar is nonetheless manually changed, a warning
should be displayed if there is a mismatch between both calendars.
| -rw-r--r-- | addons/hr_contract/__manifest__.py | 1 | ||||
| -rw-r--r-- | addons/hr_contract/models/__init__.py | 1 | ||||
| -rw-r--r-- | addons/hr_contract/models/hr_contract.py | 24 | ||||
| -rw-r--r-- | addons/hr_contract/models/hr_employee.py | 9 | ||||
| -rw-r--r-- | addons/hr_contract/models/resource.py | 29 | ||||
| -rw-r--r-- | addons/hr_contract/static/src/css/calendar_mismatch.scss | 18 | ||||
| -rw-r--r-- | addons/hr_contract/tests/__init__.py | 1 | ||||
| -rw-r--r-- | addons/hr_contract/tests/common.py | 16 | ||||
| -rw-r--r-- | addons/hr_contract/tests/test_auto_status.py | 10 | ||||
| -rw-r--r-- | addons/hr_contract/tests/test_calendar_sync.py | 68 | ||||
| -rw-r--r-- | addons/hr_contract/tests/test_contract.py | 21 | ||||
| -rw-r--r-- | addons/hr_contract/views/hr_contract_templates.xml | 8 | ||||
| -rw-r--r-- | addons/hr_contract/views/hr_contract_views.xml | 25 |
13 files changed, 212 insertions, 19 deletions
diff --git a/addons/hr_contract/__manifest__.py b/addons/hr_contract/__manifest__.py index 1d51ac0b9ca..d229c773b53 100644 --- a/addons/hr_contract/__manifest__.py +++ b/addons/hr_contract/__manifest__.py @@ -23,6 +23,7 @@ You can assign several contracts per employee. 'security/ir.model.access.csv', 'data/hr_contract_data.xml', 'views/hr_contract_views.xml', + 'views/hr_contract_templates.xml', ], 'demo': ['data/hr_contract_demo.xml'], 'installable': True, diff --git a/addons/hr_contract/models/__init__.py b/addons/hr_contract/models/__init__.py index 6541bf23a31..e38ab2a4586 100644 --- a/addons/hr_contract/models/__init__.py +++ b/addons/hr_contract/models/__init__.py @@ -4,3 +4,4 @@ from . import hr_employee from . import hr_contract from . import res_users +from . import resource diff --git a/addons/hr_contract/models/hr_contract.py b/addons/hr_contract/models/hr_contract.py index f8662c1f55f..544ec534715 100644 --- a/addons/hr_contract/models/hr_contract.py +++ b/addons/hr_contract/models/hr_contract.py @@ -47,6 +47,12 @@ class Contract(models.Model): visa_expire = fields.Date('Visa Expire Date', related="employee_id.visa_expire", readonly=False) hr_responsible_id = fields.Many2one('res.users', 'HR Responsible', tracking=True, help='Person responsible for validating the employee\'s contracts.') + calendar_mismatch = fields.Boolean(compute='_compute_calendar_mismatch') + + @api.depends('employee_id.resource_calendar_id', 'resource_calendar_id') + def _compute_calendar_mismatch(self): + for contract in self: + contract.calendar_mismatch = contract.resource_calendar_id != contract.employee_id.resource_calendar_id def _expand_states(self, states, domain, order): return [key for key, val in type(self).state.selection] @@ -113,12 +119,28 @@ class Contract(models.Model): }) return True + @api.model + def create(self, vals_list): + contracts = super().create(vals_list) + open_contracts = contracts.filtered(lambda c: c.state in ['open', 'pending']) + # sync contract -> employee + for contract in open_contracts: + contract.employee_id.contract_id = contract + # sync contract calendar -> calendar employee + for contract in open_contracts.filtered(lambda c: c.resource_calendar_id): + contract.employee_id.resource_calendar_id = contract.resource_calendar_id + return contracts + @api.multi def write(self, vals): + res = super(Contract, self).write(vals) if vals.get('state') == 'open': for contract in self: contract.employee_id.sudo().write({'contract_id': contract.id}) - return super(Contract, self).write(vals) + calendar = vals.get('resource_calendar_id') + if calendar and self.state in ['open', 'pending']: + self.mapped('employee_id').write({'resource_calendar_id': calendar}) + return res @api.multi def _track_subtype(self, init_values): diff --git a/addons/hr_contract/models/hr_employee.py b/addons/hr_contract/models/hr_employee.py index 02531df04b8..4d596f046e0 100644 --- a/addons/hr_contract/models/hr_employee.py +++ b/addons/hr_contract/models/hr_employee.py @@ -12,6 +12,7 @@ class Employee(models.Model): contract_ids = fields.One2many('hr.contract', 'employee_id', string='Employee Contracts') contract_id = fields.Many2one('hr.contract', string='Current Contract', groups="hr.group_hr_user", help='Current contract of the employee') + calendar_mismatch = fields.Boolean(related='contract_id.calendar_mismatch') contracts_count = fields.Integer(compute='_compute_contracts_count', string='Contract Count') def _compute_contracts_count(self): @@ -39,3 +40,11 @@ class Employee(models.Model): Returns the contracts of all employees between date_from and date_to """ return self.search([])._get_contracts(date_from, date_to, states=states) + + def write(self, vals): + res = super(Employee, self).write(vals) + if vals.get('contract_id'): + for employee in self: + employee.resource_calendar_id.transfer_leaves_to(employee.contract_id.resource_calendar_id, employee.resource_id) + employee.resource_calendar_id = employee.contract_id.resource_calendar_id + return res diff --git a/addons/hr_contract/models/resource.py b/addons/hr_contract/models/resource.py new file mode 100644 index 00000000000..9382b7e5217 --- /dev/null +++ b/addons/hr_contract/models/resource.py @@ -0,0 +1,29 @@ +# -*- coding:utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from datetime import datetime + +from odoo import models, fields, api +from odoo.osv.expression import AND + + +class ResourceCalendar(models.Model): + _inherit = 'resource.calendar' + + @api.multi + def transfer_leaves_to(self, other_calendar, resources=None, from_date=None): + """ + Transfer some resource.calendar.leaves from 'self' to another calendar 'other_calendar'. + Transfered leaves linked to `resources` (or all if `resources` is None) and starting + after 'from_date' (or today if None). + """ + from_date = from_date or fields.Datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + domain = [ + ('calendar_id', 'in', self.ids), + ('date_from', '>=', from_date), + ] + domain = AND([domain, [('resource_id', 'in', resources.ids)]]) if resources else domain + + self.env['resource.calendar.leaves'].search(domain).write({ + 'calendar_id': other_calendar.id, + }) + diff --git a/addons/hr_contract/static/src/css/calendar_mismatch.scss b/addons/hr_contract/static/src/css/calendar_mismatch.scss new file mode 100644 index 00000000000..f2570454198 --- /dev/null +++ b/addons/hr_contract/static/src/css/calendar_mismatch.scss @@ -0,0 +1,18 @@ +.o_form_view { + .o_calendar_warning { + cursor: help; + } + + .o_calendar_warning_tooltip { + position: absolute; + opacity: 0; + visibility: hidden; + } + + .o_calendar_warning:hover + .o_calendar_warning_tooltip { + position: relative; + visibility: visible; + opacity: 1; + transition: visibility 0s, opacity 0.5s linear; + } +} diff --git a/addons/hr_contract/tests/__init__.py b/addons/hr_contract/tests/__init__.py index ad627a612a4..d798e53356b 100644 --- a/addons/hr_contract/tests/__init__.py +++ b/addons/hr_contract/tests/__init__.py @@ -3,3 +3,4 @@ from . import test_auto_status from . import test_contract +from . import test_calendar_sync diff --git a/addons/hr_contract/tests/common.py b/addons/hr_contract/tests/common.py new file mode 100644 index 00000000000..3c7dc0b80e9 --- /dev/null +++ b/addons/hr_contract/tests/common.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo.tests.common import TransactionCase +from dateutil.relativedelta import relativedelta + + +class TestContractBase(TransactionCase): + + def setUp(self): + super(TestContractBase, self).setUp() + + self.employee = self.env['hr.employee'].create({ + 'name': 'Richard', + 'gender': 'male', + 'country_id': self.ref('base.be'), + }) diff --git a/addons/hr_contract/tests/test_auto_status.py b/addons/hr_contract/tests/test_auto_status.py index 20b8e4007fc..c342d05dd0f 100644 --- a/addons/hr_contract/tests/test_auto_status.py +++ b/addons/hr_contract/tests/test_auto_status.py @@ -1,21 +1,15 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from datetime import date, datetime -from odoo.tests.common import TransactionCase from dateutil.relativedelta import relativedelta +from odoo.addons.hr_contract.tests.common import TestContractBase -class TestHrContracts(TransactionCase): +class TestHrContracts(TestContractBase): def setUp(self): super(TestHrContracts, self).setUp() self.contracts = self.env['hr.contract'].with_context(tracking_disable=True) - self.employee = self.env['hr.employee'].create({ - 'name': 'Richard', - 'gender': 'male', - 'birthday': '1984-05-01', - 'country_id': self.ref('base.be'), - }) self.test_contract = dict(name='Test', wage=1, employee_id=self.employee.id, state='open') def apply_cron(self): diff --git a/addons/hr_contract/tests/test_calendar_sync.py b/addons/hr_contract/tests/test_calendar_sync.py new file mode 100644 index 00000000000..b025def4c7e --- /dev/null +++ b/addons/hr_contract/tests/test_calendar_sync.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.fields import Datetime, Date +from odoo.addons.hr_contract.tests.common import TestContractBase + + +class TestContractCalendars(TestContractBase): + + def setUp(self): + super(TestContractCalendars, self).setUp() + self.calendar_richard = self.env['resource.calendar'].create({'name': 'Calendar of Richard'}) + self.employee.resource_calendar_id = self.calendar_richard + + self.calendar_35h = self.env['resource.calendar'].create({'name': '35h calendar'}) + self.calendar_35h._onchange_hours_per_day() # update hours/day + + self.contract_cdd = self.env['hr.contract'].create({ + 'date_end': Date.to_date('2015-11-15'), + 'date_start': Date.to_date('2015-01-01'), + 'name': 'First CDD Contract for Richard', + 'resource_calendar_id': self.calendar_35h.id, + 'wage': 5000.0, + 'employee_id': self.employee.id, + 'state': 'close', + }) + + def test_contract_state_incoming_to_open(self): + # Employee's calendar should change + self.assertEqual(self.employee.resource_calendar_id, self.calendar_richard) + self.contract_cdd.state = 'open' + self.assertEqual(self.employee.resource_calendar_id, self.contract_cdd.resource_calendar_id, "The employee should have the calendar of its contract.") + + def test_contract_transfer_leaves(self): + + def create_calendar_leave(start, end, resource=None): + return self.env['resource.calendar.leaves'].create({ + 'name': 'leave name', + 'date_from': start, + 'date_to': end, + 'resource_id': resource.id if resource else None, + 'calendar_id': self.employee.resource_calendar_id.id, + 'time_type': 'leave', + }) + + start = Datetime.to_datetime('2015-11-17 07:00:00') + end = Datetime.to_datetime('2015-11-20 18:00:00') + leave1 = create_calendar_leave(start, end, resource=self.employee.resource_id) + + start = Datetime.to_datetime('2015-11-25 07:00:00') + end = Datetime.to_datetime('2015-11-28 18:00:00') + leave2 = create_calendar_leave(start, end, resource=self.employee.resource_id) + + # global leave + start = Datetime.to_datetime('2015-11-25 07:00:00') + end = Datetime.to_datetime('2015-11-28 18:00:00') + leave3 = create_calendar_leave(start, end) + + self.calendar_richard.transfer_leaves_to(self.calendar_35h, resources=self.employee.resource_id, from_date=Date.to_date('2015-11-21')) + + self.assertEqual(leave1.calendar_id, self.calendar_richard, "It should stay in Richard's calendar") + self.assertEqual(leave3.calendar_id, self.calendar_richard, "Global leave should stay in original calendar") + self.assertEqual(leave2.calendar_id, self.calendar_35h, "It should be transfered to the other calendar") + + # Transfer global leaves + self.calendar_richard.transfer_leaves_to(self.calendar_35h, resources=None, from_date=Date.to_date('2015-11-21')) + + self.assertEqual(leave3.calendar_id, self.calendar_35h, "Global leave should be transfered")
\ No newline at end of file diff --git a/addons/hr_contract/tests/test_contract.py b/addons/hr_contract/tests/test_contract.py index 39d6743d275..ca05600929c 100644 --- a/addons/hr_contract/tests/test_contract.py +++ b/addons/hr_contract/tests/test_contract.py @@ -1,22 +1,16 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from datetime import datetime -from odoo.tests.common import TransactionCase +from datetime import datetime, date from odoo.exceptions import ValidationError from dateutil.relativedelta import relativedelta +from odoo.addons.hr_contract.tests.common import TestContractBase -class TestHrContracts(TransactionCase): +class TestHrContracts(TestContractBase): def setUp(self): super(TestHrContracts, self).setUp() self.contracts = self.env['hr.contract'].with_context(tracking_disable=True) - self.employee = self.env['hr.employee'].create({ - 'name': 'Richard', - 'gender': 'male', - 'birthday': '1984-05-01', - 'country_id': self.ref('base.be'), - }) def create_contract(self, state, start, end=None): return self.env['hr.contract'].create({ @@ -84,3 +78,12 @@ class TestHrContracts(TransactionCase): with self.assertRaises(ValidationError): # No end self.create_contract('incoming', datetime.strptime('2015-01-01', '%Y-%m-%d').date()) + + def test_set_employee_contract_create(self): + contract = self.create_contract('open', date(2018, 1, 1), date(2018, 1, 2)) + self.assertEqual(self.employee.contract_id, contract) + + def test_set_employee_contract_write(self): + contract = self.create_contract('draft', date(2018, 1, 1), date(2018, 1, 2)) + contract.state = 'open' + self.assertEqual(self.employee.contract_id, contract) diff --git a/addons/hr_contract/views/hr_contract_templates.xml b/addons/hr_contract/views/hr_contract_templates.xml new file mode 100644 index 00000000000..90e7e2249b6 --- /dev/null +++ b/addons/hr_contract/views/hr_contract_templates.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <template id="assets_backend" name="hr_contract_assets_backend" inherit_id="web.assets_backend"> + <xpath expr="." position="inside"> + <link rel="stylesheet" type="text/scss" href="/hr_contract/static/src/css/calendar_mismatch.scss"/> + </xpath> + </template> +</odoo> diff --git a/addons/hr_contract/views/hr_contract_views.xml b/addons/hr_contract/views/hr_contract_views.xml index a94f091f82f..4fe8099acd7 100644 --- a/addons/hr_contract/views/hr_contract_views.xml +++ b/addons/hr_contract/views/hr_contract_views.xml @@ -37,6 +37,19 @@ <xpath expr="//field[@name='bank_account_id']" position="replace"> <field name="bank_account_id" context="{'display_partner':True}" attrs="{'invisible' : [('address_home_id', '=', False)]}"/> </xpath> + <xpath expr="//field[@name='resource_calendar_id']" position="replace"> + <field name="calendar_mismatch" invisible="1"/> + <label for="resource_calendar_id"/> + <div> + <field name="resource_calendar_id" required="1" nolabel="1"/> + <span attrs="{'invisible': [('calendar_mismatch', '=', False)]}" + class="fa fa-exclamation-triangle text-danger o_calendar_warning pl-3"> + </span> + <span class="o_calendar_warning_tooltip text-danger"> + Calendar Mismatch : The employee's calendar does not match its current contract calendar. This could lead to unexpected behaviors. + </span> + </div> + </xpath> </data> </field> </record> @@ -127,7 +140,17 @@ <span class="text-muted">(If fixed-term contract)</span> </div> <field name="trial_date_end"/> - <field name="resource_calendar_id"/> + <field name="calendar_mismatch" invisible="1"/> + <label for="resource_calendar_id"/> + <div> + <field name="resource_calendar_id" required="1" nolabel="1"/> + <span attrs="{'invisible': ['|', ('calendar_mismatch', '=', False), ('state', 'not in', ['open', 'pending'])]}" + class="fa fa-exclamation-triangle text-danger o_calendar_warning pl-3"> + </span> + <span class="o_calendar_warning_tooltip text-danger"> + Calendar Mismatch : The employee's calendar does not match this contract's calendar. This could lead to unexpected behaviors. + </span> + </div> <field name="hr_responsible_id"/> </group> </group> |
