diff --git a/addons/hr_contract/data/hr_contract_demo.xml b/addons/hr_contract/data/hr_contract_demo.xml
index 6e8326a0203..eef59fd9813 100644
--- a/addons/hr_contract/data/hr_contract_demo.xml
+++ b/addons/hr_contract/data/hr_contract_demo.xml
@@ -11,7 +11,7 @@
- open
+ draft
@@ -64,7 +64,7 @@
- open
+ draft
diff --git a/addons/hr_contract/models/hr_contract.py b/addons/hr_contract/models/hr_contract.py
index 21aaa77487d..938ae2d06a8 100644
--- a/addons/hr_contract/models/hr_contract.py
+++ b/addons/hr_contract/models/hr_contract.py
@@ -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()
diff --git a/addons/hr_contract/tests/__init__.py b/addons/hr_contract/tests/__init__.py
index 6f1832811c2..ad627a612a4 100644
--- a/addons/hr_contract/tests/__init__.py
+++ b/addons/hr_contract/tests/__init__.py
@@ -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
diff --git a/addons/hr_contract/tests/test_auto_status.py b/addons/hr_contract/tests/test_auto_status.py
index 580a6d17423..20b8e4007fc 100644
--- a/addons/hr_contract/tests/test_auto_status.py
+++ b/addons/hr_contract/tests/test_auto_status.py
@@ -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')
diff --git a/addons/hr_contract/tests/test_contract.py b/addons/hr_contract/tests/test_contract.py
new file mode 100644
index 00000000000..39d6743d275
--- /dev/null
+++ b/addons/hr_contract/tests/test_contract.py
@@ -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())
diff --git a/addons/hr_payroll/models/hr_benefit.py b/addons/hr_payroll/models/hr_benefit.py
index bd99e999a12..c25ebfd8afb 100644
--- a/addons/hr_payroll/models/hr_benefit.py
+++ b/addons/hr_payroll/models/hr_benefit.py
@@ -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,
diff --git a/addons/hr_payroll/models/hr_contract.py b/addons/hr_payroll/models/hr_contract.py
index 3fb14c8988f..53217a4b4b9 100644
--- a/addons/hr_payroll/models/hr_contract.py
+++ b/addons/hr_payroll/models/hr_contract.py
@@ -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):
"""
diff --git a/addons/hr_payroll/models/hr_employee.py b/addons/hr_payroll/models/hr_employee.py
index 2ab7a292149..ec3d6df3198 100644
--- a/addons/hr_payroll/models/hr_employee.py
+++ b/addons/hr_payroll/models/hr_employee.py
@@ -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',
})
diff --git a/addons/hr_payroll/models/hr_leave.py b/addons/hr_payroll/models/hr_leave.py
index aaa49384762..b659386f985 100644
--- a/addons/hr_payroll/models/hr_leave.py
+++ b/addons/hr_payroll/models/hr_leave.py
@@ -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)
diff --git a/addons/hr_payroll/models/hr_payslip.py b/addons/hr_payroll/models/hr_payslip.py
index 110432fcadf..de7ce63761e 100644
--- a/addons/hr_payroll/models/hr_payslip.py
+++ b/addons/hr_payroll/models/hr_payslip.py
@@ -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)
diff --git a/addons/hr_payroll/models/resource.py b/addons/hr_payroll/models/resource.py
index 72593757d06..4763312c4f5 100644
--- a/addons/hr_payroll/models/resource.py
+++ b/addons/hr_payroll/models/resource.py
@@ -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'
diff --git a/addons/hr_payroll/tests/__init__.py b/addons/hr_payroll/tests/__init__.py
index 1a7d794de5a..98f0f93e77b 100644
--- a/addons/hr_payroll/tests/__init__.py
+++ b/addons/hr_payroll/tests/__init__.py
@@ -3,3 +3,5 @@
from . import test_payslip_flow
from . import test_benefit
+from . import test_multi_contract
+from . import test_calendar_sync
diff --git a/addons/hr_payroll/tests/common.py b/addons/hr_payroll/tests/common.py
index 3848011849c..c64419282e5 100644
--- a/addons/hr_payroll/tests/common.py
+++ b/addons/hr_payroll/tests/common.py
@@ -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',
+ })
diff --git a/addons/hr_payroll/tests/test_benefit.py b/addons/hr_payroll/tests/test_benefit.py
index 7e7ea89382c..92a5d1daa6b 100644
--- a/addons/hr_payroll/tests/test_benefit.py
+++ b/addons/hr_payroll/tests/test_benefit.py
@@ -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,
diff --git a/addons/hr_payroll/tests/test_calendar_sync.py b/addons/hr_payroll/tests/test_calendar_sync.py
new file mode 100644
index 00000000000..f108fd90eec
--- /dev/null
+++ b/addons/hr_payroll/tests/test_calendar_sync.py
@@ -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")
\ No newline at end of file
diff --git a/addons/hr_payroll/tests/test_multi_contract.py b/addons/hr_payroll/tests/test_multi_contract.py
new file mode 100644
index 00000000000..75f07871e45
--- /dev/null
+++ b/addons/hr_payroll/tests/test_multi_contract.py
@@ -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.")
diff --git a/addons/hr_payroll/views/hr_benefit_views.xml b/addons/hr_payroll/views/hr_benefit_views.xml
index f3cb80eae0c..5b05cff1f11 100644
--- a/addons/hr_payroll/views/hr_benefit_views.xml
+++ b/addons/hr_payroll/views/hr_benefit_views.xml
@@ -82,6 +82,7 @@
+
diff --git a/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py b/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py
index 35647d78265..6cbb6c80d78 100644
--- a/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py
+++ b/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py
@@ -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'
diff --git a/addons/hr_payroll_account/models/hr_payroll_account.py b/addons/hr_payroll_account/models/hr_payroll_account.py
index 82117e38cdd..9263b8f69e3 100644
--- a/addons/hr_payroll_account/models/hr_payroll_account.py
+++ b/addons/hr_payroll_account/models/hr_payroll_account.py
@@ -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
diff --git a/addons/hr_payroll_account/tests/test_hr_payroll_account.py b/addons/hr_payroll_account/tests/test_hr_payroll_account.py
index 5d86e607440..0813ffd635d 100644
--- a/addons/hr_payroll_account/tests/test_hr_payroll_account.py
+++ b/addons/hr_payroll_account/tests/test_hr_payroll_account.py
@@ -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.