[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.
This commit is contained in:
Lucas Lefèvre
2019-06-13 10:02:10 +02:00
committed by Yannick Tivisse
parent 55e7b702bb
commit 22437e331a
13 changed files with 212 additions and 19 deletions
+1
View File
@@ -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,
+1
View File
@@ -4,3 +4,4 @@
from . import hr_employee
from . import hr_contract
from . import res_users
from . import resource
+23 -1
View File
@@ -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):
+9
View File
@@ -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
+29
View File
@@ -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,
})
@@ -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;
}
}
+1
View File
@@ -3,3 +3,4 @@
from . import test_auto_status
from . import test_contract
from . import test_calendar_sync
+16
View File
@@ -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'),
})
+2 -8
View File
@@ -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):
@@ -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")
+12 -9
View File
@@ -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)
@@ -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>
+24 -1
View File
@@ -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>