[IMP] hr_payroll: add multi contract support
Purpose ======= We would like to be able to manage multi contracts payslips and contract transitions. Specification ============= General flow: 1/ Benefits are generated based on contracts's calendar. 2/ When validating additionnal benefits, calendar.attendances are created in the contract's calendar 3/ Create one payslip per contract in state [open|pending|close] 4/ For each payslip, generate worked day lines based on the contract's calendar New Constraints: 1/ An employee cannot have more than one contract at the same time (excluding draft, closed and cancelled contracts). 2/ A leave cannot overlap multiple contracts. 3/ A contract cannot have starting and ending dates such that a leave become in the middle of two contacts. New state for a contract: Incoming. It means it's ready and will be running soon. An incoming contract automatically becomes 'Running' if the start date is passed (cron) Number of hours and days are always correct (according to the contract's calendar) on payslips as the flow starts from the contract's calendar. However, it is not correct in the Time Off app because it's counted based on the employee's calendar. The number of days/hours for hr.leaves should be computed according to the contract at the time of the leave. e.g. If I currently work full time and I request 2 weeks off next month where I'll be working only 3 days/week and 5hours/day (other contract), It should count 6 days and 30 hours. - in hr.benefits form view show the contract (below employee field) TaskID: 1903483 closes odoo/odoo#30181
This commit is contained in:
committed by
Yannick Tivisse
parent
f5bca867ac
commit
20bc2bbab5
@@ -11,7 +11,7 @@
|
||||
<field name="department_id" model="hr.department"
|
||||
eval="obj().env.ref('hr.employee_admin').department_id.id"/>
|
||||
<field eval="7540.0" name="wage"/>
|
||||
<field name="state">open</field>
|
||||
<field name="state">draft</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_contract_al" model="hr.contract">
|
||||
@@ -64,7 +64,7 @@
|
||||
<field name="department_id" model="hr.department"
|
||||
eval="obj().env.ref('hr.employee_qdp').department_id.id"/>
|
||||
<field eval="3750.0" name="wage"/>
|
||||
<field name="state">open</field>
|
||||
<field name="state">draft</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_contract_han" model="hr.contract">
|
||||
|
||||
@@ -16,15 +16,9 @@ class Employee(models.Model):
|
||||
medic_exam = fields.Date(string='Medical Examination Date', groups="hr.group_hr_user")
|
||||
vehicle = fields.Char(string='Company Vehicle', groups="hr.group_hr_user")
|
||||
contract_ids = fields.One2many('hr.contract', 'employee_id', string='Employee Contracts')
|
||||
contract_id = fields.Many2one('hr.contract', compute='_compute_contract_id', string='Current Contract', help='Latest contract of the employee')
|
||||
contract_id = fields.Many2one('hr.contract', string='Current Contract', help='Current contract of the employee')
|
||||
contracts_count = fields.Integer(compute='_compute_contracts_count', string='Contract Count')
|
||||
|
||||
def _compute_contract_id(self):
|
||||
""" get the lastest contract """
|
||||
Contract = self.env['hr.contract']
|
||||
for employee in self:
|
||||
employee.contract_id = Contract.search([('employee_id', '=', employee.id)], order='date_start desc', limit=1)
|
||||
|
||||
def _compute_contracts_count(self):
|
||||
# read_group as sudo, since contract count is displayed on form view
|
||||
contract_data = self.env['hr.contract'].sudo().read_group([('employee_id', 'in', self.ids)], ['employee_id'], ['employee_id'])
|
||||
@@ -32,25 +26,28 @@ class Employee(models.Model):
|
||||
for employee in self:
|
||||
employee.contracts_count = result.get(employee.id, 0)
|
||||
|
||||
def _get_contracts(self, date_from, date_to):
|
||||
def _get_contracts(self, date_from, date_to, states=['open', 'pending']):
|
||||
"""
|
||||
Returns the contracts of the employee between date_from and date_to
|
||||
"""
|
||||
# a contract is valid if it ends between the given dates
|
||||
if not self:
|
||||
employees = self.env['hr.employee'].search([])
|
||||
else:
|
||||
employees = self
|
||||
clause_1 = ['&', ('date_end', '<=', date_to), ('date_end', '>=', date_from)]
|
||||
# OR if it starts between the given dates
|
||||
clause_2 = ['&', ('date_start', '<=', date_to), ('date_start', '>=', date_from)]
|
||||
# OR if it starts before the date_from and finish after the date_end (or never finish)
|
||||
clause_3 = ['&', ('date_start', '<=', date_from), '|', ('date_end', '=', False), ('date_end', '>=', date_to)]
|
||||
clause_final = expression.AND([
|
||||
[('employee_id', 'in', employees.ids), ('state', '=', 'open')],
|
||||
[('employee_id', 'in', self.ids), ('state', 'in', states)],
|
||||
expression.OR([clause_1, clause_2, clause_3])])
|
||||
return self.env['hr.contract'].search(clause_final)
|
||||
|
||||
@api.model
|
||||
def _get_all_contracts(self, date_from, date_to, states=['open', 'pending']):
|
||||
"""
|
||||
Returns the contracts of all employees between date_from and date_to
|
||||
"""
|
||||
return self.search([])._get_contracts(date_from, date_to, states=states)
|
||||
|
||||
|
||||
class ContractType(models.Model):
|
||||
|
||||
@@ -88,6 +85,7 @@ class Contract(models.Model):
|
||||
notes = fields.Text('Notes')
|
||||
state = fields.Selection([
|
||||
('draft', 'New'),
|
||||
('incoming', 'Incoming'),
|
||||
('open', 'Running'),
|
||||
('pending', 'To Renew'),
|
||||
('close', 'Expired'),
|
||||
@@ -112,6 +110,27 @@ class Contract(models.Model):
|
||||
self.department_id = self.employee_id.department_id
|
||||
self.resource_calendar_id = self.employee_id.resource_calendar_id
|
||||
|
||||
@api.constrains('employee_id', 'state', 'date_start', 'date_end')
|
||||
def _check_current_contract(self):
|
||||
""" Two contracts in state [incoming | pending | open | close] cannot overlap """
|
||||
for contract in self.filtered(lambda c: c.state not in ['draft', 'cancel']):
|
||||
domain = [
|
||||
('id', '!=', contract.id),
|
||||
('employee_id', '=', contract.employee_id.id),
|
||||
('state', 'in', ['incoming', 'pending', 'open', 'close']),
|
||||
]
|
||||
|
||||
if not contract.date_end:
|
||||
start_domain = []
|
||||
end_domain = ['|', ('date_end', '>=', contract.date_start), ('date_end', '=', False)]
|
||||
else:
|
||||
start_domain = [('date_start', '<=', contract.date_end)]
|
||||
end_domain = ['|', ('date_end', '>', contract.date_start), ('date_end', '=', False)]
|
||||
|
||||
domain = expression.AND([domain, start_domain, end_domain])
|
||||
if self.search_count(domain):
|
||||
raise ValidationError(_('An employee can only have one contract at the same time. (Excluding Draft and Cancelled contracts)'))
|
||||
|
||||
@api.constrains('date_start', 'date_end')
|
||||
def _check_dates(self):
|
||||
if self.filtered(lambda c: c.date_end and c.date_start > c.date_end):
|
||||
@@ -141,8 +160,18 @@ class Contract(models.Model):
|
||||
'state': 'close'
|
||||
})
|
||||
|
||||
self.search([('state', '=', 'incoming'), ('date_start', '<=', fields.Date.to_string(date.today())),]).write({
|
||||
'state': 'open'
|
||||
})
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
if vals.get('state') == 'open':
|
||||
for contract in self:
|
||||
contract.employee_id.contract_id = contract
|
||||
return super(Contract, self).write(vals)
|
||||
|
||||
@api.multi
|
||||
def _track_subtype(self, init_values):
|
||||
self.ensure_one()
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_auto_status
|
||||
from . import test_contract
|
||||
|
||||
@@ -10,7 +10,12 @@ class TestHrContracts(TransactionCase):
|
||||
def setUp(self):
|
||||
super(TestHrContracts, self).setUp()
|
||||
self.contracts = self.env['hr.contract'].with_context(tracking_disable=True)
|
||||
self.employee = self.env.ref('hr.employee_admin')
|
||||
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):
|
||||
@@ -51,3 +56,9 @@ class TestHrContracts(TransactionCase):
|
||||
self.contract.write(self.test_contract)
|
||||
self.apply_cron()
|
||||
self.assertEquals(self.contract.state, 'close')
|
||||
|
||||
def test_contract_start_date(self):
|
||||
self.test_contract.update(dict(date_start=datetime.now(), state='incoming'))
|
||||
self.contract = self.contracts.create(self.test_contract)
|
||||
self.apply_cron()
|
||||
self.assertEquals(self.contract.state, 'open')
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# -*- 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 odoo.exceptions import ValidationError
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
|
||||
class TestHrContracts(TransactionCase):
|
||||
|
||||
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({
|
||||
'name': 'Contract',
|
||||
'employee_id': self.employee.id,
|
||||
'state': state,
|
||||
'wage': 1,
|
||||
'date_start': start,
|
||||
'date_end': end,
|
||||
})
|
||||
|
||||
def test_incoming_overlapping_contract(self):
|
||||
start = datetime.strptime('2015-11-01', '%Y-%m-%d').date()
|
||||
end = datetime.strptime('2015-11-30', '%Y-%m-%d').date()
|
||||
self.create_contract('open', 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('incoming', start, end)
|
||||
|
||||
def test_pending_overlapping_contract(self):
|
||||
start = datetime.strptime('2015-11-01', '%Y-%m-%d').date()
|
||||
end = datetime.strptime('2015-11-30', '%Y-%m-%d').date()
|
||||
self.create_contract('open', start, end)
|
||||
|
||||
# Pending contract
|
||||
with self.assertRaises(ValidationError, msg="It should not create two contract in state open or pending"):
|
||||
start = datetime.strptime('2015-11-15', '%Y-%m-%d').date()
|
||||
end = datetime.strptime('2015-12-30', '%Y-%m-%d').date()
|
||||
self.create_contract('pending', start, end)
|
||||
|
||||
# Draft contract -> should not raise
|
||||
start = datetime.strptime('2015-11-15', '%Y-%m-%d').date()
|
||||
end = datetime.strptime('2015-12-30', '%Y-%m-%d').date()
|
||||
self.create_contract('draft', start, end)
|
||||
|
||||
def test_draft_overlapping_contract(self):
|
||||
start = datetime.strptime('2015-11-01', '%Y-%m-%d').date()
|
||||
end = datetime.strptime('2015-11-30', '%Y-%m-%d').date()
|
||||
self.create_contract('open', start, end)
|
||||
|
||||
# Draft contract -> should not raise even if overlapping
|
||||
start = datetime.strptime('2015-11-15', '%Y-%m-%d').date()
|
||||
end = datetime.strptime('2015-12-30', '%Y-%m-%d').date()
|
||||
self.create_contract('draft', start, end)
|
||||
|
||||
def test_overlapping_contract_no_end(self):
|
||||
|
||||
# No end date
|
||||
self.create_contract('open', datetime.strptime('2015-11-01', '%Y-%m-%d').date())
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
start = datetime.strptime('2015-11-15', '%Y-%m-%d').date()
|
||||
end = datetime.strptime('2015-12-30', '%Y-%m-%d').date()
|
||||
self.create_contract('incoming', start, end)
|
||||
|
||||
def test_overlapping_contract_no_end2(self):
|
||||
|
||||
start = datetime.strptime('2015-11-01', '%Y-%m-%d').date()
|
||||
end = datetime.strptime('2015-12-30', '%Y-%m-%d').date()
|
||||
self.create_contract('open', start, end)
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
# No end
|
||||
self.create_contract('incoming', datetime.strptime('2015-01-01', '%Y-%m-%d').date())
|
||||
@@ -21,6 +21,7 @@ class HrBenefit(models.Model):
|
||||
date_start = fields.Datetime(required=True, string='From')
|
||||
date_stop = fields.Datetime(string='To')
|
||||
duration = fields.Float(compute='_compute_duration', inverse='_inverse_duration', store=True, string="Period")
|
||||
contract_id = fields.Many2one('hr.contract', string="Contract", required=True)
|
||||
benefit_type_id = fields.Many2one('hr.benefit.type')
|
||||
color = fields.Integer(related='benefit_type_id.color', readonly=True)
|
||||
state = fields.Selection([
|
||||
@@ -190,6 +191,7 @@ class HrBenefit(models.Model):
|
||||
'name': benefit.name,
|
||||
'employee_id': benefit.employee_id.id,
|
||||
'benefit_type_id': benefit.benefit_type_id.id,
|
||||
'contract_id': benefit.contract_id.id,
|
||||
}
|
||||
benefit_state = benefit.state
|
||||
benefit.unlink()
|
||||
@@ -256,7 +258,7 @@ class HrBenefit(models.Model):
|
||||
'date_to': end.date(),
|
||||
'hour_from':start.hour + start.minute/60,
|
||||
'hour_to': end.hour + end.minute/60,
|
||||
'calendar_id': benefit.employee_id.resource_calendar_id.id,
|
||||
'calendar_id': benefit.contract_id.resource_calendar_id.id,
|
||||
'day_period': 'morning' if end.hour <= 12 else 'afternoon',
|
||||
'resource_id': benefit.employee_id.resource_id.id,
|
||||
'benefit_type_id': benefit.benefit_type_id.id,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
from datetime import date
|
||||
|
||||
class HrContract(models.Model):
|
||||
_inherit = 'hr.contract'
|
||||
@@ -25,6 +25,18 @@ class HrContract(models.Model):
|
||||
full_time_required_hours = fields.Float(related='resource_calendar_id.full_time_required_hours')
|
||||
is_fulltime = fields.Boolean(related='resource_calendar_id.is_fulltime')
|
||||
|
||||
@api.constrains('date_start', 'date_end', 'state')
|
||||
def _check_contracts(self):
|
||||
self._get_leaves()._check_contracts()
|
||||
|
||||
@api.multi
|
||||
def _get_leaves(self):
|
||||
return self.env['hr.leave'].search([
|
||||
('employee_id', 'in', self.mapped('employee_id.id')),
|
||||
('date_from', '<=', max([end or date.max for end in self.mapped('date_end')])),
|
||||
('date_to', '>=', min(self.mapped('date_start'))),
|
||||
])
|
||||
|
||||
@api.multi
|
||||
def get_all_structures(self):
|
||||
"""
|
||||
|
||||
@@ -33,8 +33,14 @@ class HrEmployee(models.Model):
|
||||
('state', 'in', ['draft', 'confirmed'])
|
||||
]))
|
||||
|
||||
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
res = super(HrEmployee, 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
|
||||
|
||||
@api.model
|
||||
def generate_benefit(self, date_start, date_stop):
|
||||
@@ -47,7 +53,7 @@ class HrEmployee(models.Model):
|
||||
date_start = _format_datetime(date_start)
|
||||
date_stop = _format_datetime(date_stop)
|
||||
|
||||
current_contracts = self.env['hr.employee']._get_contracts(date_start, date_stop)
|
||||
current_contracts = self.env['hr.employee']._get_all_contracts(date_start, date_stop, states=['open', 'pending', 'close'])
|
||||
current_employees = current_contracts.mapped('employee_id')
|
||||
mapped_data = dict.fromkeys(current_employees, self.env['hr.contract'])
|
||||
|
||||
@@ -55,6 +61,7 @@ class HrEmployee(models.Model):
|
||||
mapped_data[contract.employee_id] |= contract
|
||||
|
||||
for employee, contracts in mapped_data.items():
|
||||
|
||||
# Approved leaves
|
||||
emp_leaves = employee.resource_calendar_id.leave_ids.filtered(
|
||||
lambda r:
|
||||
@@ -63,9 +70,7 @@ class HrEmployee(models.Model):
|
||||
r.date_to.replace(tzinfo=pytz.utc) >= date_start
|
||||
)
|
||||
global_leaves = employee.resource_calendar_id.global_leave_ids
|
||||
for calendar_leave in emp_leaves | global_leaves:
|
||||
hr_leave = calendar_leave.holiday_id
|
||||
hr_leave.copy_to_benefits()
|
||||
(emp_leaves | global_leaves).mapped('holiday_id').copy_to_benefits()
|
||||
|
||||
new_benefits = self.env['hr.benefit']
|
||||
for contract in contracts:
|
||||
@@ -85,6 +90,7 @@ class HrEmployee(models.Model):
|
||||
'date_stop': interval[1].astimezone(pytz.utc),
|
||||
'benefit_type_id': benefit_type_id.id,
|
||||
'employee_id': employee.id,
|
||||
'contract_id': contract.id,
|
||||
'state': 'confirmed',
|
||||
})
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import math
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from datetime import datetime, date
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.addons.resource.models.resource import Intervals
|
||||
from odoo.addons.resource.models.resource import HOURS_PER_DAY
|
||||
|
||||
class HrLeaveType(models.Model):
|
||||
_inherit = 'hr.leave.type'
|
||||
@@ -16,23 +20,87 @@ class HrLeave(models.Model):
|
||||
@api.multi
|
||||
def copy_to_benefits(self):
|
||||
for leave in self:
|
||||
contract = leave.employee_id._get_contracts(leave.date_from, leave.date_to, states=['open', 'pending', 'close'])
|
||||
start = max(leave.date_from, datetime.combine(contract.date_start, datetime.min.time()))
|
||||
end = min(leave.date_to, datetime.combine(contract.date_end or date.max, datetime.max.time()))
|
||||
benefit_type = leave.holiday_status_id.benefit_type_id
|
||||
self.env['hr.benefit'].safe_duplicate_create({
|
||||
'name': "%s%s" % (benefit_type.name + ": " if benefit_type else "", leave.employee_id.name),
|
||||
'date_start': leave.date_from,
|
||||
'date_stop': leave.date_to,
|
||||
'date_start': start,
|
||||
'date_stop': end,
|
||||
'benefit_type_id': benefit_type.id,
|
||||
'employee_id': leave.employee_id.id,
|
||||
'leave_id': self.id,
|
||||
'leave_id': leave.id,
|
||||
'state': 'confirmed',
|
||||
'contract_id': contract.id,
|
||||
})
|
||||
|
||||
@api.multi
|
||||
def _create_resource_leave(self):
|
||||
"""
|
||||
Add a resource leave in calendars of contracts running at the same period.
|
||||
This is needed in order to compute the correct number of hours/days of the leave
|
||||
according to the contract's calender.
|
||||
"""
|
||||
super(HrLeave, self)._create_resource_leave()
|
||||
resource_leave_values = []
|
||||
|
||||
for leave in self.filtered(lambda l: l.employee_id):
|
||||
|
||||
contract = leave.employee_id._get_contracts(leave.date_from, leave.date_to, states=['open', 'pending', 'close'])
|
||||
if contract and contract.resource_calendar_id != leave.employee_id.resource_calendar_id:
|
||||
resource_leave_values += [{
|
||||
'name': leave.name,
|
||||
'holiday_id': leave.id,
|
||||
'resource_id': leave.employee_id.resource_id.id,
|
||||
'benefit_type_id': leave.holiday_status_id.benefit_type_id.id,
|
||||
'time_type': leave.holiday_status_id.time_type,
|
||||
'date_from': max(leave.date_from, datetime.combine(contract.date_start, datetime.min.time())),
|
||||
'date_to': min(leave.date_to, datetime.combine(contract.date_end or date.max, datetime.max.time())),
|
||||
'calendar_id': contract.resource_calendar_id.id,
|
||||
}]
|
||||
|
||||
self.env['resource.calendar.leaves'].create(resource_leave_values)
|
||||
return True
|
||||
|
||||
@api.constrains('date_from', 'date_to')
|
||||
def _check_contracts(self):
|
||||
"""
|
||||
A leave cannot be set across multiple contracts.
|
||||
Note: a leave can be across multiple contracts despite this constraint.
|
||||
It happens if a leave is correctly created (not accross multiple contracts) but
|
||||
contracts are later modifed/created in the middle of the leave.
|
||||
"""
|
||||
for holiday in self:
|
||||
domain = [
|
||||
('employee_id', '=', holiday.employee_id.id),
|
||||
('date_start', '<=', holiday.date_to),
|
||||
('state', 'not in', ['draft', 'cancel']),
|
||||
'|',
|
||||
('date_end', '>=', holiday.date_from),
|
||||
('date_end', '=', False),
|
||||
]
|
||||
nbr_contracts = self.env['hr.contract'].sudo().search_count(domain)
|
||||
if nbr_contracts > 1:
|
||||
raise ValidationError(_('A leave cannot be set across multiple contracts.'))
|
||||
|
||||
@api.multi
|
||||
def _cancel_benefit_conflict(self):
|
||||
"""
|
||||
Unlink any benefit linked to a leave in self.
|
||||
Re-create new benefits where the leaves do not cover the full range of the deleted benefits.
|
||||
Create a leave benefit for each leave in self.
|
||||
Return True if one or more benefits are unlinked.
|
||||
e.g.:
|
||||
|---------------- benefit ----------------|
|
||||
|------ leave ------|
|
||||
||
|
||||
vv
|
||||
|-benef-|---benefit leave---|----benefit---|
|
||||
"""
|
||||
benefits = self.env['hr.benefit'].search([('leave_id', 'in', self.ids)])
|
||||
if benefits:
|
||||
self.copy_to_benefits()
|
||||
|
||||
# create new benefits where the leave does not cover the full benefit
|
||||
benefits_intervals = Intervals(intervals=[(b.date_start, b.date_stop, b) for b in benefits])
|
||||
leave_intervals = Intervals(intervals=[(l.date_from, l.date_to, l) for l in self])
|
||||
@@ -52,17 +120,18 @@ class HrLeave(models.Model):
|
||||
'date_start': benefit_start,
|
||||
'date_stop': benefit_stop,
|
||||
'benefit_type_id': benefit_type.id,
|
||||
'contract_id': benefit.contract_id.id,
|
||||
'employee_id': employee.id,
|
||||
'state': 'confirmed',
|
||||
})
|
||||
benefits.unlink()
|
||||
return True
|
||||
return False
|
||||
|
||||
@api.multi
|
||||
def action_validate(self):
|
||||
super(HrLeave, self).action_validate()
|
||||
self._cancel_benefit_conflict()
|
||||
calendar_leaves = self.env['resource.calendar.leaves'].search([('holiday_id', 'in', self.ids)])
|
||||
calendar_leaves.write({'benefit_type_id': self.holiday_status_id.benefit_type_id.id})
|
||||
self._cancel_benefit_conflict() # delete preexisting conflicting benefits
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
@@ -71,3 +140,32 @@ class HrLeave(models.Model):
|
||||
benefits = self.env['hr.benefit'].search([('leave_id', 'in', self.ids)])
|
||||
benefits.write({'display_warning': False, 'active': False})
|
||||
return True
|
||||
|
||||
def _get_number_of_days(self, date_from, date_to, employee_id):
|
||||
""" If an employee is currently working full time but requests a leave next month
|
||||
where he has a new contract working only 3 days/week. This should be taken into
|
||||
account when computing the number of days for the leave (2 weeks leave = 6 days).
|
||||
Override this method to get number of days according to the contract's calendar
|
||||
at the time of the leave.
|
||||
"""
|
||||
days = super(HrLeave, self)._get_number_of_days(date_from, date_to, employee_id)
|
||||
if employee_id:
|
||||
employee = self.env['hr.employee'].browse(employee_id)
|
||||
# Use sudo otherwise base users can't compute number of days
|
||||
contracts = employee.sudo()._get_contracts(date_from, date_to, states=['incoming', 'open', 'pending'])
|
||||
calendar = contracts[:1].resource_calendar_id if contracts else None # Note: if len(contracts)>1, the leave creation will crash because of unicity constaint
|
||||
return employee.get_work_days_data(date_from, date_to, calendar=calendar)['days']
|
||||
|
||||
return days
|
||||
|
||||
@api.multi
|
||||
@api.depends('number_of_days')
|
||||
def _compute_number_of_hours_display(self):
|
||||
""" Override for the same reason as _get_number_of_days()"""
|
||||
super(HrLeave, self)._compute_number_of_hours_display()
|
||||
for holiday in self:
|
||||
if holiday.date_from and holiday.date_to:
|
||||
contracts = holiday.employee_id.sudo()._get_contracts(holiday.date_from, holiday.date_to, states=['incoming', 'open', 'pending'])
|
||||
contract_calendar = contracts[:1].resource_calendar_id if contracts else None
|
||||
calendar = contract_calendar or holiday.employee_id.resource_calendar_id or self.env.user.company_id.resource_calendar_id
|
||||
holiday.number_of_hours_display = holiday.number_of_days * (calendar.hours_per_day or HOURS_PER_DAY)
|
||||
|
||||
@@ -23,7 +23,7 @@ class HrPayslip(models.Model):
|
||||
'to the contract chosen. If you let empty the field contract, this field isn\'t '
|
||||
'mandatory anymore and thus the rules applied will be all the rules set on the '
|
||||
'structure of all contracts of the employee valid for the chosen period')
|
||||
name = fields.Char(string='Payslip Name', readonly=True,
|
||||
name = fields.Char(string='Payslip Name', readonly=True, required=True,
|
||||
states={'draft': [('readonly', False)], 'verify': [('readonly', False)]})
|
||||
number = fields.Char(string='Reference', readonly=True, copy=False,
|
||||
states={'draft': [('readonly', False)], 'verify': [('readonly', False)]})
|
||||
@@ -143,31 +143,27 @@ class HrPayslip(models.Model):
|
||||
number = payslip.number or self.env['ir.sequence'].next_by_code('salary.slip')
|
||||
# delete old payslip lines
|
||||
payslip.line_ids.unlink()
|
||||
# set the list of contract for which the rules have to be applied
|
||||
# if we don't give the contract, then the rules to apply should be for all current contracts of the employee
|
||||
contracts = payslip.contract_id or \
|
||||
payslip.employee_id._get_contracts(payslip.date_from, payslip.date_to)
|
||||
lines = [(0, 0, line) for line in payslip._get_payslip_lines(contracts)]
|
||||
lines = [(0, 0, line) for line in payslip._get_payslip_lines(payslip.contract_id)]
|
||||
payslip.write({'line_ids': lines, 'number': number, 'state': 'verify', 'compute_date': fields.Date.today()})
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def get_worked_day_lines(self, contracts, date_from, date_to):
|
||||
def get_worked_day_lines(self, contract, date_from, date_to):
|
||||
"""
|
||||
@param contract: Browse record of contracts
|
||||
@return: returns a list of dict containing the worked days that should be applied for the given contract between date_from and date_to
|
||||
"""
|
||||
res = []
|
||||
# fill only if the contract as a working schedule linked
|
||||
for contract in contracts.filtered(lambda contract: contract.resource_calendar_id):
|
||||
day_from = datetime.combine(fields.Date.from_string(date_from), time.min)
|
||||
day_to = datetime.combine(fields.Date.from_string(date_to), time.max)
|
||||
if contract.resource_calendar_id:
|
||||
day_from = datetime.combine(max(fields.Date.from_string(date_from), contract.date_start), time.min)
|
||||
day_to = datetime.combine(min(fields.Date.from_string(date_to), contract.date_end or date.max), time.max)
|
||||
|
||||
calendar = contract.resource_calendar_id
|
||||
|
||||
benefit_types = self.env['hr.benefit.type'].search([('code', '!=', False)])
|
||||
for benefit_type in benefit_types:
|
||||
hours = contract.employee_id.get_benefit_days_data(benefit_type, day_from, day_to)['hours']
|
||||
hours = contract.employee_id.get_benefit_days_data(benefit_type, day_from, day_to, calendar=calendar)['hours']
|
||||
if hours:
|
||||
line = {
|
||||
'name': benefit_type.name,
|
||||
@@ -265,7 +261,7 @@ class HrPayslip(models.Model):
|
||||
|
||||
return list(result_dict.values())
|
||||
|
||||
@api.onchange('employee_id', 'date_from', 'date_to')
|
||||
@api.onchange('employee_id', 'contract_id', 'date_from', 'date_to')
|
||||
def onchange_employee(self):
|
||||
|
||||
if (not self.employee_id) or (not self.date_from) or (not self.date_to):
|
||||
@@ -274,22 +270,20 @@ class HrPayslip(models.Model):
|
||||
employee = self.employee_id
|
||||
date_from = self.date_from
|
||||
date_to = self.date_to
|
||||
|
||||
|
||||
self.company_id = employee.company_id
|
||||
|
||||
contracts = self.env['hr.contract']
|
||||
if not self.env.context.get('contract') or not self.contract_id:
|
||||
if not self.contract_id: # Add a default contract if not already defined
|
||||
contracts = employee._get_contracts(date_from, date_to)
|
||||
if not contracts:
|
||||
|
||||
if not contracts or not contracts[0].struct_id:
|
||||
return
|
||||
self.contract_id = contracts[0]
|
||||
self.struct_id = contracts[0].struct_id
|
||||
|
||||
if not self.contract_id.struct_id:
|
||||
return
|
||||
self.struct_id = self.contract_id.struct_id
|
||||
|
||||
#computation of the salary worked days
|
||||
worked_days_line_ids = self.get_worked_day_lines(contracts, date_from, date_to)
|
||||
worked_days_line_ids = self.get_worked_day_lines(self.contract_id, date_from, date_to)
|
||||
worked_days_lines = self.worked_days_line_ids.browse([])
|
||||
for r in worked_days_line_ids:
|
||||
worked_days_lines += worked_days_lines.new(r)
|
||||
@@ -302,13 +296,6 @@ class HrPayslip(models.Model):
|
||||
payslip_name = self.struct_id.payslip_name or _('Salary Slip')
|
||||
self.name = '%s - %s - %s' % (payslip_name, self.employee_id.name, tools.ustr(babel.dates.format_date(date=ttyme, format='MMMM-y', locale=locale)))
|
||||
|
||||
@api.onchange('contract_id')
|
||||
def onchange_contract(self):
|
||||
if not self.contract_id:
|
||||
self.struct_id = False
|
||||
self.with_context(contract=True).onchange_employee()
|
||||
return
|
||||
|
||||
def get_salary_line_total(self, code):
|
||||
self.ensure_one()
|
||||
line = self.line_ids.filtered(lambda line: line.code == code)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import models, fields, api
|
||||
from odoo.osv.expression import AND, OR
|
||||
from odoo.osv.expression import AND
|
||||
from odoo.tools import float_compare
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class ResourceCalendar(models.Model):
|
||||
_inherit = 'resource.calendar'
|
||||
@@ -39,6 +41,25 @@ class ResourceCalendar(models.Model):
|
||||
def _onchange_hours_per_day(self):
|
||||
return super(ResourceCalendar, self)._onchange_hours_per_day()
|
||||
|
||||
@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 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,
|
||||
})
|
||||
|
||||
|
||||
class ResourceCalendarAttendance(models.Model):
|
||||
_inherit = 'resource.calendar.attendance'
|
||||
|
||||
|
||||
@@ -3,3 +3,5 @@
|
||||
|
||||
from . import test_payslip_flow
|
||||
from . import test_benefit
|
||||
from . import test_multi_contract
|
||||
from . import test_calendar_sync
|
||||
|
||||
@@ -47,3 +47,64 @@ class TestPayslipBase(TransactionCase):
|
||||
'employee_id': self.richard_emp.id,
|
||||
'struct_id': self.developer_pay_structure.id,
|
||||
})
|
||||
|
||||
self.benefit_type_leave = self.env['hr.benefit.type'].create({
|
||||
'name': 'Leave',
|
||||
'is_leave': True,
|
||||
'code': 'LEAVE100'
|
||||
})
|
||||
self.leave_type = self.env['hr.leave.type'].create({
|
||||
'name': 'Legal Leaves',
|
||||
'time_type': 'leave',
|
||||
'allocation_type': 'no',
|
||||
'benefit_type_id': self.benefit_type_leave.id
|
||||
})
|
||||
|
||||
class TestPayslipContractBase(TestPayslipBase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestPayslipContractBase, self).setUp()
|
||||
self.calendar_richard = self.env['resource.calendar'].create({'name': 'Calendar of Richard'})
|
||||
self.calendar_40h = self.env['resource.calendar'].create({'name': 'Default calendar'})
|
||||
self.calendar_35h = self.env['resource.calendar'].create({
|
||||
'name': '35h calendar',
|
||||
'attendance_ids': [
|
||||
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Monday Evening', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
|
||||
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Tuesday Evening', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
|
||||
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Wednesday Evening', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
|
||||
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Thursday Evening', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}),
|
||||
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}),
|
||||
(0, 0, {'name': 'Friday Evening', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'})
|
||||
]
|
||||
})
|
||||
self.calendar_35h._onchange_hours_per_day() # update hours/day
|
||||
self.richard_emp.resource_calendar_id = self.calendar_richard
|
||||
|
||||
# This contract ends at the 15th of the month
|
||||
self.contract_cdd = self.env['hr.contract'].create({ # Fixed term contract
|
||||
'date_end': datetime.strptime('2015-11-15', '%Y-%m-%d'),
|
||||
'date_start': datetime.strptime('2015-01-01', '%Y-%m-%d'),
|
||||
'name': 'First CDD Contract for Richard',
|
||||
'resource_calendar_id': self.calendar_40h.id,
|
||||
'wage': 5000.0,
|
||||
'type_id': self.ref('hr_contract.hr_contract_type_emp'),
|
||||
'employee_id': self.richard_emp.id,
|
||||
'struct_id': self.developer_pay_structure.id,
|
||||
'state': 'close',
|
||||
})
|
||||
|
||||
# This contract starts the next day
|
||||
self.contract_cdi = self.env['hr.contract'].create({ # Fixed term contract
|
||||
'date_start': datetime.strptime('2015-11-16', '%Y-%m-%d'),
|
||||
'name': 'Contract for Richard',
|
||||
'resource_calendar_id': self.calendar_35h.id,
|
||||
'wage': 5000.0,
|
||||
'type_id': self.ref('hr_contract.hr_contract_type_emp'),
|
||||
'employee_id': self.richard_emp.id,
|
||||
'struct_id': self.developer_pay_structure.id,
|
||||
'state': 'open',
|
||||
})
|
||||
|
||||
@@ -18,7 +18,7 @@ class TestBenefit(TestPayslipBase):
|
||||
self.start = datetime.strptime('2015-11-01 01:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz)
|
||||
self.end = datetime.strptime('2015-11-30 23:59:59', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz)
|
||||
self.resource_calendar_id = self.env['resource.calendar'].create({'name': 'Zboub'})
|
||||
self.env['hr.contract'].create({
|
||||
contract = self.env['hr.contract'].create({
|
||||
'date_start': self.start - relativedelta(days=5),
|
||||
'name': 'dodo',
|
||||
'resource_calendar_id': self.resource_calendar_id.id,
|
||||
@@ -28,18 +28,7 @@ class TestBenefit(TestPayslipBase):
|
||||
'state': 'open',
|
||||
})
|
||||
self.richard_emp.resource_calendar_id = self.resource_calendar_id
|
||||
|
||||
self.benefit_type_leave = self.env['hr.benefit.type'].create({
|
||||
'name': 'Leave',
|
||||
'is_leave': True,
|
||||
'code': 'LEAVE100'
|
||||
})
|
||||
self.leave_type = self.env['hr.leave.type'].create({
|
||||
'name': 'Paid Time Off',
|
||||
'time_type': 'leave',
|
||||
'allocation_type': 'no',
|
||||
'benefit_type_id': self.benefit_type_leave.id
|
||||
})
|
||||
self.richard_emp.contract_id = contract
|
||||
self.benefit_type = self.env['hr.benefit.type'].create({
|
||||
'name': 'Extra attendance',
|
||||
'is_leave': False,
|
||||
@@ -69,6 +58,7 @@ class TestBenefit(TestPayslipBase):
|
||||
benefit = self.env['hr.benefit'].create({
|
||||
'name': '1',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'contract_id': self.richard_emp.contract_id.id,
|
||||
'date_start': start,
|
||||
'date_stop': end,
|
||||
})
|
||||
@@ -91,6 +81,7 @@ class TestBenefit(TestPayslipBase):
|
||||
benefit = self.env['hr.benefit'].create({
|
||||
'name': '1',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'contract_id': self.richard_emp.contract_id.id,
|
||||
'date_start': start,
|
||||
'date_stop': end,
|
||||
})
|
||||
@@ -114,6 +105,7 @@ class TestBenefit(TestPayslipBase):
|
||||
benefit = self.env['hr.benefit'].create({
|
||||
'name': '1',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'contract_id': self.richard_emp.contract_id.id,
|
||||
'date_start': start,
|
||||
'date_stop': end,
|
||||
'benefit_type_id': self.benefit_type.id,
|
||||
@@ -131,6 +123,7 @@ class TestBenefit(TestPayslipBase):
|
||||
'name': '1',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'benefit_type_id': self.env.ref('hr_payroll.benefit_type_attendance').id,
|
||||
'contract_id': self.richard_emp.contract_id.id,
|
||||
'date_start': start,
|
||||
'date_stop': end,
|
||||
})
|
||||
@@ -150,6 +143,7 @@ class TestBenefit(TestPayslipBase):
|
||||
'name': '1',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'benefit_type_id': self.benefit_type.id,
|
||||
'contract_id': self.richard_emp.contract_id.id,
|
||||
'date_start': start,
|
||||
'date_stop': end,
|
||||
})
|
||||
@@ -186,6 +180,7 @@ class TestBenefit(TestPayslipBase):
|
||||
'name': 'Richard leave from benef',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'benefit_type_id': self.benefit_type_leave.id,
|
||||
'contract_id': self.richard_emp.contract_id.id,
|
||||
'date_start': start,
|
||||
'date_stop': end,
|
||||
})
|
||||
@@ -201,6 +196,7 @@ class TestBenefit(TestPayslipBase):
|
||||
'name': '1',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'benefit_type_id': self.env.ref('hr_payroll.benefit_type_attendance').id,
|
||||
'contract_id': self.richard_emp.contract_id.id,
|
||||
'date_start': start,
|
||||
'date_stop': end + relativedelta(hours=5),
|
||||
})
|
||||
@@ -208,6 +204,7 @@ class TestBenefit(TestPayslipBase):
|
||||
'name': '2',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'benefit_type_id': self.env.ref('hr_payroll.benefit_type_attendance').id,
|
||||
'contract_id': self.richard_emp.contract_id.id,
|
||||
'date_start': start + relativedelta(hours=3),
|
||||
'date_stop': end,
|
||||
})
|
||||
@@ -220,6 +217,7 @@ class TestBenefit(TestPayslipBase):
|
||||
'name': '1',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'benefit_type_id': self.benefit_type_leave.id,
|
||||
'contract_id': self.richard_emp.contract_id.id,
|
||||
'date_start': self.start,
|
||||
'date_stop': self.end,
|
||||
})
|
||||
@@ -238,6 +236,7 @@ class TestBenefit(TestPayslipBase):
|
||||
benef1 = self.env['hr.benefit'].create({
|
||||
'name': '1',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'contract_id': self.richard_emp.contract_id.id,
|
||||
'date_start': self.start,
|
||||
'date_stop': self.end,
|
||||
})
|
||||
@@ -257,10 +256,11 @@ class TestBenefit(TestPayslipBase):
|
||||
benef = self.env['hr.benefit'].create({
|
||||
'name': '1',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'contract_id': self.richard_emp.contract_id.id,
|
||||
'benefit_type_id': self.benefit_type.id,
|
||||
'date_start': start,
|
||||
'date_stop': end,
|
||||
'leave_id': leave.id
|
||||
'leave_id': leave.id, # benefit conflicts with this leave
|
||||
})
|
||||
leave.action_approve()
|
||||
|
||||
@@ -295,6 +295,7 @@ class TestBenefit(TestPayslipBase):
|
||||
benef = self.env['hr.benefit'].create({
|
||||
'name': '1',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'contract_id': self.richard_emp.contract_id.id,
|
||||
'benefit_type_id': self.benefit_type.id,
|
||||
'date_start': start,
|
||||
'date_stop': end,
|
||||
@@ -316,6 +317,7 @@ class TestBenefit(TestPayslipBase):
|
||||
benef = self.env['hr.benefit'].create({
|
||||
'name': '1',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'contract_id': self.richard_emp.contract_id.id,
|
||||
'benefit_type_id': self.benefit_type.id,
|
||||
'date_start': start,
|
||||
'date_stop': end,
|
||||
@@ -331,6 +333,7 @@ class TestBenefit(TestPayslipBase):
|
||||
leave_benef = self.env['hr.benefit'].create({
|
||||
'name': '1leave',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'contract_id': self.richard_emp.contract_id.id,
|
||||
'benefit_type_id': self.benefit_type_leave.id,
|
||||
'date_start': start,
|
||||
'date_stop': end,
|
||||
@@ -346,6 +349,7 @@ class TestBenefit(TestPayslipBase):
|
||||
leave_benef = self.env['hr.benefit'].create({
|
||||
'name': '1leave',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'contract_id': self.richard_emp.contract_id.id,
|
||||
'benefit_type_id': self.benefit_type_leave.id,
|
||||
'date_start': start,
|
||||
'date_stop': end,
|
||||
@@ -361,6 +365,7 @@ class TestBenefit(TestPayslipBase):
|
||||
leave_benef = self.env['hr.benefit'].create({
|
||||
'name': '1leave',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'contract_id': self.richard_emp.contract_id.id,
|
||||
'benefit_type_id': self.benefit_type_leave.id,
|
||||
'date_start': start,
|
||||
'date_stop': end,
|
||||
@@ -384,6 +389,7 @@ class TestBenefit(TestPayslipBase):
|
||||
benef = self.env['hr.benefit'].create({
|
||||
'name': 'Extra',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'contract_id': self.richard_emp.contract_id.id,
|
||||
'benefit_type_id': self.benefit_type.id,
|
||||
'date_start': start,
|
||||
'date_stop': end,
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
from odoo.tools import config, test_reports
|
||||
from odoo.tests.common import tagged
|
||||
from odoo.addons.hr_payroll.tests.common import TestPayslipContractBase
|
||||
|
||||
class TestPayslipCalendars(TestPayslipContractBase):
|
||||
|
||||
|
||||
def test_contract_state_incoming_to_open(self):
|
||||
# Employee's calendar should change
|
||||
self.assertEqual(self.richard_emp.resource_calendar_id, self.calendar_richard)
|
||||
self.contract_cdd.state = 'open'
|
||||
self.assertEqual(self.richard_emp.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.richard_emp.resource_calendar_id.id,
|
||||
'benefit_type_id': self.benefit_type_leave.id,
|
||||
'time_type': 'leave',
|
||||
})
|
||||
|
||||
start = datetime.strptime('2015-11-17 07:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
end = datetime.strptime('2015-11-20 18:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
leave1 = create_calendar_leave(start, end, resource=self.richard_emp.resource_id)
|
||||
|
||||
start = datetime.strptime('2015-11-25 07:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
end = datetime.strptime('2015-11-28 18:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
leave2 = create_calendar_leave(start, end, resource=self.richard_emp.resource_id)
|
||||
|
||||
# global leave
|
||||
start = datetime.strptime('2015-11-25 07:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
end = datetime.strptime('2015-11-28 18:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
leave3 = create_calendar_leave(start, end)
|
||||
|
||||
self.calendar_richard.transfer_leaves_to(self.calendar_35h, resources=self.richard_emp.resource_id, from_date=datetime.strptime('2015-11-21', '%Y-%m-%d').date())
|
||||
|
||||
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=datetime.strptime('2015-11-21', '%Y-%m-%d').date())
|
||||
|
||||
self.assertEqual(leave3.calendar_id, self.calendar_35h, "Global leave should be transfered")
|
||||
@@ -0,0 +1,149 @@
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
from odoo.tools import config, test_reports
|
||||
from odoo.tests.common import tagged
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.addons.hr_payroll.tests.common import TestPayslipContractBase
|
||||
|
||||
@tagged('payslips_multi_contract')
|
||||
class TestPayslipMultiContract(TestPayslipContractBase):
|
||||
|
||||
def create_leave(self, start, end):
|
||||
return self.env['hr.leave'].create({
|
||||
'name': 'Doctor Appointment',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'holiday_status_id': self.leave_type.id,
|
||||
'date_from': start,
|
||||
'date_to': end,
|
||||
})
|
||||
|
||||
def test_multi_contract(self):
|
||||
# First contact: 40h, start of the month
|
||||
payslip = self.env['hr.payslip'].create({
|
||||
'name': 'November 2015',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'date_from': datetime.strptime('2015-11-01', '%Y-%m-%d'),
|
||||
'date_to': datetime.strptime('2015-11-30', '%Y-%m-%d'),
|
||||
'contract_id': self.contract_cdd.id,
|
||||
})
|
||||
payslip.onchange_employee()
|
||||
self.assertEqual(payslip.worked_days_line_ids.number_of_hours, 80, "It should be 80 hours of work this month for this contract")
|
||||
self.assertEqual(payslip.worked_days_line_ids.number_of_days, 10, "It should be 10 days of work this month for this contract")
|
||||
|
||||
# Second contract: 35h, end of the month
|
||||
payslip = self.env['hr.payslip'].create({
|
||||
'name': 'November 2015',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'date_from': datetime.strptime('2015-11-01', '%Y-%m-%d'),
|
||||
'date_to': datetime.strptime('2015-11-30', '%Y-%m-%d'),
|
||||
'contract_id': self.contract_cdi.id,
|
||||
})
|
||||
payslip.onchange_employee()
|
||||
self.assertEqual(payslip.worked_days_line_ids.number_of_hours, 77, "It should be 77 hours of work this month for this contract")
|
||||
self.assertEqual(payslip.worked_days_line_ids.number_of_days, 11, "It should be 11 days of work this month for this contract")
|
||||
|
||||
def test_multi_contract_holiday(self):
|
||||
# Leave during second contract
|
||||
leave = self.env['resource.calendar.leaves'].create({
|
||||
'name': 'leave name',
|
||||
'date_from': datetime.strptime('2015-11-17 07:00:00', '%Y-%m-%d %H:%M:%S'),
|
||||
'date_to': datetime.strptime('2015-11-20 18:00:00', '%Y-%m-%d %H:%M:%S'),
|
||||
'resource_id': self.richard_emp.resource_id.id,
|
||||
'calendar_id': self.calendar_35h.id,
|
||||
'benefit_type_id': self.benefit_type_leave.id,
|
||||
'time_type': 'leave',
|
||||
})
|
||||
payslip = self.env['hr.payslip'].create({
|
||||
'name': 'November 2015',
|
||||
'employee_id': self.richard_emp.id,
|
||||
'date_from': datetime.strptime('2015-11-01', '%Y-%m-%d'),
|
||||
'date_to': datetime.strptime('2015-11-30', '%Y-%m-%d'),
|
||||
'contract_id': self.contract_cdi.id,
|
||||
})
|
||||
payslip.onchange_employee()
|
||||
work = payslip.worked_days_line_ids.filtered(lambda line: line.code == 'WORK100')
|
||||
leave = payslip.worked_days_line_ids.filtered(lambda line: line.code == 'LEAVE100')
|
||||
self.assertEqual(work.number_of_hours, 49, "It should be 49 hours of work this month for this contract")
|
||||
self.assertEqual(leave.number_of_hours, 28, "It should be 28 hours of leave this month for this contract")
|
||||
self.assertEqual(work.number_of_days, 7, "It should be 7 days of work this month for this contract")
|
||||
self.assertEqual(leave.number_of_days, 4, "It should be 4 days of leave this month for this contract")
|
||||
|
||||
def test_move_contract_in_leave(self):
|
||||
# test move contract dates such that a leave is accross two contracts
|
||||
start = datetime.strptime('2015-11-05 07:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
end = datetime.strptime('2015-12-15 18:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
self.contract_cdi.write({'date_start': datetime.strptime('2015-12-30', '%Y-%m-%d').date()})
|
||||
print(self.contract_cdi.date_start, 'test')
|
||||
# begins during contract, ends after contract
|
||||
leave = self.create_leave(start, end)
|
||||
leave.action_approve()
|
||||
# move contract in the middle of the leave
|
||||
with self.assertRaises(ValidationError):
|
||||
self.contract_cdi.date_start = datetime.strptime('2015-11-17', '%Y-%m-%d').date()
|
||||
|
||||
def test_create_contract_in_leave(self):
|
||||
# test create contract such that a leave is accross two contracts
|
||||
start = datetime.strptime('2015-11-05 07:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
end = datetime.strptime('2015-12-15 18:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
self.contract_cdi.date_start = datetime.strptime('2015-12-30', '%Y-%m-%d').date() # remove this contract to be able to create the leave
|
||||
# begins during contract, ends after contract
|
||||
leave = self.create_leave(start, end)
|
||||
leave.action_approve()
|
||||
# move contract in the middle of the leave
|
||||
with self.assertRaises(ValidationError):
|
||||
contract = self.env['hr.contract'].create({
|
||||
'date_start': datetime.strptime('2015-11-30', '%Y-%m-%d').date(),
|
||||
'name': 'Contract for Richard',
|
||||
'resource_calendar_id': self.calendar_40h.id,
|
||||
'wage': 5000.0,
|
||||
'type_id': self.ref('hr_contract.hr_contract_type_emp'),
|
||||
'employee_id': self.richard_emp.id,
|
||||
'struct_id': self.developer_pay_structure.id,
|
||||
'state': 'open',
|
||||
})
|
||||
|
||||
def test_leave_outside_contract(self):
|
||||
|
||||
# Leave outside contract => should not raise
|
||||
start = datetime.strptime('2014-10-18 07:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
end = datetime.strptime('2014-10-20 09:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
self.create_leave(start, end)
|
||||
|
||||
# begins before contract, ends during contract => should not raise
|
||||
start = datetime.strptime('2014-10-25 07:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
end = datetime.strptime('2015-01-15 18:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
self.create_leave(start, end)
|
||||
|
||||
# begins during contract, ends after contract => should not raise
|
||||
self.contract_cdi.date_end = datetime.strptime('2015-11-30', '%Y-%m-%d').date()
|
||||
start = datetime.strptime('2015-11-25 07:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
end = datetime.strptime('2015-12-5 18:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
self.create_leave(start, end)
|
||||
|
||||
def test_no_leave_overlapping_contracts(self):
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
# Overlap two contracts
|
||||
start = datetime.strptime('2015-11-12 07:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
end = datetime.strptime('2015-11-17 18:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
leave = self.create_leave(start, end)
|
||||
|
||||
# Leave inside fixed term contract => should not raise
|
||||
start = datetime.strptime('2015-11-04 07:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
end = datetime.strptime('2015-11-07 09:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
leave = self.create_leave(start, end)
|
||||
|
||||
# Leave inside contract (no end) => should not raise
|
||||
start = datetime.strptime('2015-11-18 07:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
end = datetime.strptime('2015-11-20 09:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
leave = self.create_leave(start, end)
|
||||
|
||||
def test_leave_request_next_contracts(self):
|
||||
|
||||
start = datetime.strptime('2015-11-23 07:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
end = datetime.strptime('2015-11-24 18:00:00', '%Y-%m-%d %H:%M:%S')
|
||||
leave = self.create_leave(start, end)
|
||||
|
||||
leave._compute_number_of_hours_display()
|
||||
self.assertEqual(leave.number_of_hours_display, 14, "It should count hours according to the future contract.")
|
||||
@@ -82,6 +82,7 @@
|
||||
<group>
|
||||
<group>
|
||||
<field name="employee_id" attrs="{'readonly': [('state', '!=', 'draft')]}" />
|
||||
<field name="contract_id" readonly="1"/>
|
||||
<field name="benefit_type_id" attrs="{'readonly': [('state', '=', 'validated')]}" options="{'no_create': True, 'no_open': True}"/>
|
||||
<field name="leave_id" attrs="{'invisible': [('leave_id', '=', False)]}"/>
|
||||
</group>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from datetime import datetime
|
||||
from datetime import datetime, date
|
||||
|
||||
|
||||
class HrPayslipEmployees(models.TransientModel):
|
||||
@@ -43,17 +43,20 @@ class HrPayslipEmployees(models.TransientModel):
|
||||
values = Payslip.default_get(Payslip.fields_get())
|
||||
values.update({
|
||||
'employee_id': employee.id,
|
||||
'date_from': payslip_run.date_start,
|
||||
'date_to': payslip_run.date_end,
|
||||
'credit_note': payslip_run.credit_note,
|
||||
'payslip_run_id': payslip_run.id,
|
||||
'date_from': payslip_run.date_start,
|
||||
'date_to': payslip_run.date_end,
|
||||
})
|
||||
payslip = self.env['hr.payslip'].new(values)
|
||||
payslip.onchange_employee()
|
||||
payslip.onchange_contract()
|
||||
payslip._onchange_struct_id()
|
||||
values = payslip._convert_to_write(payslip._cache)
|
||||
payslips += Payslip.create(values)
|
||||
for contract in employee._get_contracts(payslip_run.date_start, payslip_run.date_end, states=['open', 'pending', 'close']):
|
||||
values.update({
|
||||
'contract_id': contract.id,
|
||||
})
|
||||
payslip = self.env['hr.payslip'].new(values)
|
||||
payslip.onchange_employee()
|
||||
payslip._onchange_struct_id()
|
||||
values = payslip._convert_to_write(payslip._cache)
|
||||
payslips += Payslip.create(values)
|
||||
payslips.compute_sheet()
|
||||
payslip_run.state = 'verify'
|
||||
|
||||
|
||||
@@ -39,9 +39,9 @@ class HrPayslip(models.Model):
|
||||
vals['journal_id'] = self.env.context.get('journal_id')
|
||||
return super(HrPayslip, self).create(vals)
|
||||
|
||||
@api.onchange('contract_id')
|
||||
def onchange_contract(self):
|
||||
super(HrPayslip, self).onchange_contract()
|
||||
@api.onchange('employee_id', 'contract_id', 'date_from', 'date_to')
|
||||
def onchange_employee(self):
|
||||
super(HrPayslip, self).onchange_employee()
|
||||
self.journal_id = self.contract_id.journal_id.id or self.default_get(['journal_id'])['journal_id']
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -80,7 +80,6 @@ class TestHrPayrollAccount(common.TransactionCase):
|
||||
self.hr_payslip.date_from = time.strftime('%Y-%m-01')
|
||||
self.hr_payslip.date_to = str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10]
|
||||
self.hr_payslip.onchange_employee()
|
||||
self.hr_payslip.onchange_contract()
|
||||
self.hr_payslip._onchange_struct_id()
|
||||
|
||||
# I assign the amount to Input data.
|
||||
|
||||
Reference in New Issue
Block a user