summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLucas Lefèvre <lul@odoo.com>2019-03-29 12:03:11 +0100
committerYannick Tivisse <yti@odoo.com>2019-06-13 10:02:10 +0200
commit22437e331a60c81a80d619c075a9d516e980c078 (patch)
tree9178688c381961b55be5329fbc443b42fb3a0c13
parent55e7b702bb7f01c5f3bd3efbccf82c2d2c661f49 (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__.py1
-rw-r--r--addons/hr_contract/models/__init__.py1
-rw-r--r--addons/hr_contract/models/hr_contract.py24
-rw-r--r--addons/hr_contract/models/hr_employee.py9
-rw-r--r--addons/hr_contract/models/resource.py29
-rw-r--r--addons/hr_contract/static/src/css/calendar_mismatch.scss18
-rw-r--r--addons/hr_contract/tests/__init__.py1
-rw-r--r--addons/hr_contract/tests/common.py16
-rw-r--r--addons/hr_contract/tests/test_auto_status.py10
-rw-r--r--addons/hr_contract/tests/test_calendar_sync.py68
-rw-r--r--addons/hr_contract/tests/test_contract.py21
-rw-r--r--addons/hr_contract/views/hr_contract_templates.xml8
-rw-r--r--addons/hr_contract/views/hr_contract_views.xml25
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>