[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:
committed by
Yannick Tivisse
parent
55e7b702bb
commit
22437e331a
@@ -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,
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
from . import hr_employee
|
||||
from . import hr_contract
|
||||
from . import res_users
|
||||
from . import resource
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -3,3 +3,4 @@
|
||||
|
||||
from . import test_auto_status
|
||||
from . import test_contract
|
||||
from . import test_calendar_sync
|
||||
|
||||
@@ -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'),
|
||||
})
|
||||
@@ -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")
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user