[IMP] hr_payroll: Add new models hr.benefit, hr.benefit.type

Purpose
=======

For payroll purpose, we need to manage attendances.
For instance, someone is working only mondays, tuesdays and fridays, but works exceptionally an other day.
It must be recorded.

Specification
=============
The new models are used to represent attendances, extra hours and leaves (as `hr.benefit`s) in a new window called "Benefits".
The schedule of employees for the full month is visible in that view (calendar view).

First the benefits needs to be generated (via a button) based on the calendar attendances and leaves.
The manager can then check benefits, delete or add some. Only the benefit name and type can be edited (not start and end dates).
Once benefits are correct, they can be validated. Validation will fail if they are some leaves to approve or if some benefits overlap on the same day.
Benefits cannot be deleted or edited once validated.
When benefits are all validated, a button appears to generate payslips.

Closes #28261
This commit is contained in:
lul-odoo
2018-11-30 11:08:19 +01:00
committed by Yannick Tivisse
parent 5a9b8a41b6
commit 78128760c5
23 changed files with 1379 additions and 43 deletions
+6
View File
@@ -28,6 +28,12 @@
'views/report_contributionregister_templates.xml',
'views/report_payslip_templates.xml',
'views/report_payslipdetails_templates.xml',
'views/hr_benefit_views.xml',
'views/hr_benefit_views.xml',
'views/hr_leave_views.xml',
'views/resource_views.xml',
'views/hr_benefit_template.xml',
'wizard/hr_benefit_employee_views.xml',
],
'demo': ['data/hr_payroll_demo.xml'],
}
@@ -89,6 +89,20 @@
<field name="digits">4</field>
</record>
<!-- Benefits Type -->
<record id="benefit_type_attendance" model="hr.benefit.type">
<field name="name">Attendance</field>
<field name="color">0</field>
<field name="code">WORK100</field>
</record>
<record id="benefit_type_leave" model="hr.benefit.type">
<field name="name">Generic Leave</field>
<field name="code">LEAVE100</field>
<field name="color">3</field>
<field name="is_leave">True</field>
</record>
</data>
</odoo>
@@ -146,6 +146,7 @@
<field name="employee_id" ref="hr_employee_payroll"/>
<field name="notes">Default contract for marketing executives</field>
<field eval="4000.0" name="wage"/>
<field name="state">open</field>
</record>
<record id="hr_contract_gilles_gravie" model="hr.contract">
@@ -159,4 +160,71 @@
<field eval="5000.0" name="wage"/>
</record>
<record id="hr_contract_admin" model="hr.contract">
<field name="name">Contract For Mitchell Admin</field>
<field name="type_id" ref="hr_contract.hr_contract_type_emp"/>
<field name="date_start" eval="time.strftime('%Y-%m')+'-1'"/>
<field name="date_end" eval="time.strftime('%Y')+'-12-31'"/>
<field name="struct_id" ref="hr_payroll.structure_002"/>
<field name="employee_id" ref="hr.employee_admin"/>
<field name="notes">This is Mitchell Admin's contract</field>
<field eval="5500.0" name="wage"/>
<field name="state">open</field>
</record>
<!-- Benefits -->
<record id="benefit_type_home_working" model="hr.benefit.type">
<field name="name">Home Working</field>
<field name="code">WORK200</field>
<field name="color">2</field>
</record>
<record id="benefit_type_extra_hours" model="hr.benefit.type">
<field name="name">Extra hours</field>
<field name="code">WORK300</field>
<field name="color">2</field>
</record>
<record id="benefit_type_long_leave" model="hr.benefit.type">
<field name="name">Long Term Leave</field>
<field name="code">LEAVE200</field>
<field name="is_leave">True</field>
<field name="color">4</field>
</record>
<record id="benefit_type_unpaid_leave" model="hr.benefit.type">
<field name="name">Unpaid Leave</field>
<field name="code">LEAVE300</field>
<field name="is_leave">True</field>
<field name="color">5</field>
</record>
<record id="benefit_type_sick_leave" model="hr.benefit.type">
<field name="name">Sick Leave</field>
<field name="code">LEAVE400</field>
<field name="is_leave">True</field>
<field name="color">5</field>
</record>
<!-- add benefit type to leave type -->
<record id="hr_holidays.holiday_status_sl" model="hr.leave.type">
<field name="benefit_type_id" ref="benefit_type_sick_leave"></field>
</record>
<record id="hr_holidays.holiday_status_unpaid" model="hr.leave.type">
<field name="benefit_type_id" ref="benefit_type_unpaid_leave"></field>
</record>
<record id="hr_holidays.holiday_status_hw" model="hr.leave.type">
<field name="benefit_type_id" ref="benefit_type_home_working"></field>
</record>
<record id="hr_holidays.holiday_status_cl" model="hr.leave.type">
<field name="benefit_type_id" ref="benefit_type_leave"></field>
</record>
<record id="hr_holidays.holiday_status_comp" model="hr.leave.type">
<field name="benefit_type_id" ref="benefit_type_leave"></field>
</record>
</odoo>
+3
View File
@@ -7,3 +7,6 @@ from . import hr_employee
from . import res_config_settings
from . import hr_salary_rule
from . import hr_payslip
from . import hr_benefit
from . import hr_leave
from . import resource
+306
View File
@@ -0,0 +1,306 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import pytz
import itertools
from psycopg2 import IntegrityError
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, exceptions, _
from odoo.tools import mute_logger
from odoo.exceptions import UserError, ValidationError
from odoo.addons.resource.models.resource import Intervals
class HrBenefit(models.Model):
_name = 'hr.benefit'
_description = 'hr.benefit'
name = fields.Char(required=True)
active = fields.Boolean(default=True)
employee_id = fields.Many2one('hr.employee', required=True,
domain=lambda self: [('contract_ids.state', 'in', ('open', 'pending')), ('company_id', '=', self.env.user.company_id.id)])
date_start = fields.Datetime(required=True, string='Start')
date_stop = fields.Datetime(string='End')
duration = fields.Float(compute='_compute_duration', inverse='_inverse_duration', store=True, string="Hours")
benefit_type_id = fields.Many2one('hr.benefit.type')
color = fields.Integer(related='benefit_type_id.color', readonly=True)
state = fields.Selection([
('draft', 'Draft'),
('confirmed', 'Confirmed'),
('validated', 'Validated'),
('cancelled', 'Cancelled')
], default='draft')
display_warning = fields.Boolean(string="Error")
leave_id = fields.Many2one('hr.leave', string='Leave')
_sql_constraints = [
('_unique', 'unique (employee_id, date_start, date_stop, benefit_type_id)', "Benefit already exists for this attendence"),
]
@api.model
def create(self, vals):
vals['state'] = 'confirmed'
return super(HrBenefit, self).create(vals)
@api.constrains('date_stop', 'duration')
def _check_validity_benefit_ends(self):
""" verifies if benefit has an end. """
for benefit in self:
if not (benefit.date_stop or benefit.duration):
raise exceptions.ValidationError(_('Benefit must end. Please define an end date or a duration.'))
@api.constrains('date_start', 'date_stop')
def _check_validity_start_before_end(self):
""" verifies if benefit has an end. """
for benefit in self:
if benefit.date_stop < benefit.date_start:
raise exceptions.ValidationError(_('Starting time should be before end time.'))
@api.onchange('duration')
def _onchange_duration(self):
self._inverse_duration()
@api.depends('date_stop', 'date_start')
def _compute_duration(self):
for benefit in self:
if benefit.date_start and benefit.date_stop:
dt = benefit.date_stop - benefit.date_start
benefit.duration = dt.days * 24 + dt.seconds / 3600 # Number of hours
def _inverse_duration(self):
for benefit in self:
if benefit.date_start and benefit.duration:
benefit.date_stop = benefit.date_start + relativedelta(hours=benefit.duration)
@api.multi
def check_if_error(self):
if not self:
return False
undefined_type = self.filtered(lambda b: not b.benefit_type_id)
undefined_type.write({'display_warning': True})
conflict = self._compute_schedule_conflicts()
conflict_with_leaves = self.compute_conflicts_leaves_to_approve()
return undefined_type or conflict or conflict_with_leaves
@api.multi
def _compute_schedule_conflicts(self):
conflict = False
date_start_benefits = min(self.mapped('date_start'))
date_stop_benefits = max(self.mapped('date_stop'))
domain = [
('date_start', '<', date_stop_benefits),
('date_stop', '>', date_start_benefits),
]
benefs = self.search(domain)
benefits_by_employee = itertools.groupby(benefs, lambda b: b.employee_id)
for employee, benefs in benefits_by_employee:
intervals = Intervals(intervals=((b.date_start, b.date_stop, b) for b in benefs))
for interval in intervals:
if len(interval[2]) > 1:
interval[2].write({'display_warning': True})
conflict = True
return conflict
@api.multi
def compute_conflicts_leaves_to_approve(self):
if not self:
return False
query = """
SELECT
b.id AS benefit_id,
l.id AS leave_id
FROM hr_benefit b
INNER JOIN hr_leave l ON b.employee_id = l.employee_id
WHERE
b.id IN %s AND
l.date_from <= b.date_stop AND
l.date_to >= b.date_start AND
l.state IN ('confirm', 'validate1');
"""
self.env.cr.execute(query, [tuple(self.ids)])
conflicts = self.env.cr.dictfetchall()
for res in conflicts:
self.browse(res.get('benefit_id')).write({
'display_warning': True,
'leave_id': res.get('leave_id')
})
return bool(conflicts)
def safe_duplicate_create(self, vals):
"""
Create benefit but silently abord if it already exists in the database (according to the _unique constraints).
@return: new record if it didn't exist, empty recordset otherwise.
"""
try:
with mute_logger('odoo.sql_db'), self.env.cr.savepoint():
# Any database call will be run inside a postgresql savepoint which is
# rolledback if an exception occurs allowing to make subsequent calls in the transaction.
# (Otherwise InternalError is raised by any subsequent database operations)
return self.create(vals)
except IntegrityError as err:
if 'hr_benefit__unique' not in err.pgerror:
raise err
return self.env['hr.benefit']
def action_leave(self):
leave = self.leave_id
return {
'type':'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_id': leave.id,
'res_model': 'hr.leave',
'views': [[False, 'form']],
}
@api.multi
def action_draft(self):
for benefit in self:
if benefit.state not in ['confirmed', 'cancelled']:
raise UserError(_('Benefit state must be "Confirmed" or "Cancelled" in order to be reset to draft.'))
benefit.write({
'state': 'draft',
'active': True,
})
return True
@api.multi
def action_cancel(self):
for benefit in self:
if benefit.state not in ['confirmed']:
raise UserError(_('Benefit state must be "Confirmed" in order to be cancelled.'))
benefit.write({
'state': 'cancelled',
'active': False,
})
return True
def split_by_day(self):
"""
Split the benefit by days and unlink the original benefit.
@return recordset
"""
def _split_range_by_day(start, end):
days = []
current_start = start
current_end = start.replace(hour=23, minute=59, second=59)
while current_end < end:
days.append((current_start, current_end))
current_start = current_end + relativedelta(seconds=1)
current_end = current_end + relativedelta(days=1)
days.append((current_start, end))
# filter to avoid dummy intervals starting and ending at the same time
return [(start, end) for start, end in days if start != end]
new_benefits = self.env['hr.benefit']
for benefit in self:
if benefit.date_start.date() == benefit.date_stop.date():
new_benefits |= benefit
else:
tz = pytz.timezone(benefit.employee_id.tz)
benefit_start, benefit_stop = tz.localize(benefit.date_start), tz.localize(benefit.date_stop)
values = {
'name': benefit.name,
'employee_id': benefit.employee_id.id,
'benefit_type_id': benefit.benefit_type_id.id,
}
benefit_state = benefit.state
benefit.unlink()
for start, stop in _split_range_by_day(benefit_start, benefit_stop):
values['date_start'] = start.astimezone(pytz.utc)
values['date_stop'] = stop.astimezone(pytz.utc)
new_benefit = self.create(values)
# Write the state after the creation due to the ir.rule on benefit state
# LUL TODO: Write a test for this use case
new_benefit.state = benefit_state
new_benefits |= new_benefit
return new_benefits
@api.multi
def _duplicate_to_calendar(self):
"""
Duplicate data to keep the complexity in benefit and not mess up payroll, etc.
"""
attendance_benefits = self.filtered(lambda b:
not b.benefit_type_id.is_leave and
# Normal benefit are global to all employees -> avoid duplicating it
not b.benefit_type_id == self.env.ref('hr_payroll.benefit_type_attendance'))
leave_benefits = self.filtered(lambda b: b.benefit_type_id.is_leave)
for benefit in attendance_benefits:
benefit = benefit.split_by_day()
benefit._duplicate_to_calendar_attendance()
leave_benefits._duplicate_to_calendar_leave()
@api.multi
def _duplicate_to_calendar_leave(self):
for benefit in self:
if not benefit.leave_id:
tz = pytz.timezone(benefit.employee_id.tz)
self.env['resource.calendar.leaves'].create({
'name': benefit.name,
'date_from': benefit.date_start,
'date_to': benefit.date_stop,
'calendar_id': benefit.employee_id.resource_calendar_id.id,
'resource_id': benefit.employee_id.resource_id.id,
'benefit_type_id': benefit.benefit_type_id.id,
})
@api.multi
def _duplicate_to_calendar_attendance(self):
mapped_data = {
benefit: [
pytz.utc.localize(benefit.date_start).astimezone(pytz.timezone(benefit.employee_id.tz)), # Start date
pytz.utc.localize(benefit.date_stop).astimezone(pytz.timezone(benefit.employee_id.tz)) # End date
] for benefit in self
}
if any(data[0].date() != data[1].date() for data in mapped_data.values()):
raise ValidationError(_("You can't validate a benefit that covers several days."))
for benefit in self:
start, end = mapped_data.get(benefit)
self.env['resource.calendar.attendance'].create({
'name': benefit.name,
'dayofweek': str(start.weekday()),
'date_from': start.date(),
'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,
'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,
})
@api.model
def action_validate(self, ids):
benefits = self.env['hr.benefit'].search([('id', 'in', ids), ('state', '!=', 'validated')])
benefits.write({'display_warning': False})
if not benefits.check_if_error():
benefits.write({'state': 'validated'})
benefits._duplicate_to_calendar()
return True
return False
class HrBenefitType(models.Model):
_name = 'hr.benefit.type'
_description = 'hr.benefit.type'
name = fields.Char(required=True)
code = fields.Char()
color = fields.Integer(default=1) # Will be used with the new calendar/kanban view
sequence = fields.Integer(default=25)
active = fields.Boolean('Active', default=True,
help="If the active field is set to false, it will allow you to hide the benefit type without removing it.")
is_leave = fields.Boolean(default=False, string="Leave")
+47 -1
View File
@@ -1,7 +1,10 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo import api, fields, models, _
from odoo.addons.resource.models.resource import Intervals
from datetime import datetime, timedelta
import pytz
class HrEmployee(models.Model):
@@ -15,3 +18,46 @@ class HrEmployee(models.Model):
def _compute_payslip_count(self):
for employee in self:
employee.payslip_count = len(employee.slip_ids)
@api.multi
def generate_benefit(self, date_start, date_stop):
date_start = date_start.replace(tzinfo=pytz.utc)
date_stop = date_stop.replace(tzinfo=pytz.utc)
for employee in self:
# Approved leaves
emp_leaves = employee.resource_calendar_id.leave_ids.filtered(
lambda r:
r.resource_id == employee.resource_id and
r.date_from.replace(tzinfo=pytz.utc) <= date_stop and
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()
new_benefits = self.env['hr.benefit']
for contract in employee._get_contracts(date_start, date_stop):
date_start_benefits = max(date_start, datetime.combine(contract.date_start, datetime.min.time()).replace(tzinfo=pytz.utc))
date_stop_benefits = min(date_stop, datetime.combine(contract.date_end or datetime.max.date(), datetime.max.time()).replace(tzinfo=pytz.utc))
calendar = contract.resource_calendar_id
resource = employee.resource_id
attendances = calendar._work_intervals(date_start_benefits, date_stop_benefits, resource=resource)
# Attendances
for interval in attendances:
benefit_type_id = interval[2].mapped('benefit_type_id')[:1] or self.env.ref('hr_payroll.benefit_type_attendance')
new_benefits |= self.env['hr.benefit'].safe_duplicate_create({
'name': "%s: %s" % (benefit_type_id.name, employee.name),
'date_start': interval[0].astimezone(pytz.utc),
'date_stop': interval[1].astimezone(pytz.utc),
'benefit_type_id': benefit_type_id.id,
'employee_id': employee.id,
})
new_benefits.compute_conflicts_leaves_to_approve()
+71
View File
@@ -0,0 +1,71 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _
from odoo.addons.resource.models.resource import Intervals
class HrLeaveType(models.Model):
_inherit = 'hr.leave.type'
benefit_type_id = fields.Many2one('hr.benefit.type', string='Benefit Type')
class HrLeave(models.Model):
_inherit = 'hr.leave'
@api.multi
def copy_to_benefits(self):
for leave in self:
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,
'benefit_type_id': benefit_type.id,
'employee_id': leave.employee_id.id,
'leave_id': self.id,
})
@api.multi
def _cancel_benefit_conflict(self):
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])
remaining_benefits = benefits_intervals - leave_intervals
for interval in remaining_benefits:
benefit = interval[2]
leave = benefit.leave_id
benefit_type = benefit.benefit_type_id
employee = benefit.employee_id
benefit_start = interval[0] + relativedelta(seconds=1) if leave.date_to == interval[0] else interval[0]
benefit_stop = interval[1] - relativedelta(seconds=1) if leave.date_from == interval[1] else interval[1]
self.env['hr.benefit'].safe_duplicate_create({
'name': "%s: %s" % (benefit_type.name, employee.name),
'date_start': benefit_start,
'date_stop': benefit_stop,
'benefit_type_id': benefit_type.id,
'employee_id': employee.id,
})
benefits.unlink()
@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})
return True
@api.multi
def action_refuse(self):
super(HrLeave, self).action_refuse()
benefits = self.env['hr.benefit'].search([('leave_id', 'in', self.ids)])
benefits.write({'display_warning': False, 'leave_id': None})
return True
+14 -34
View File
@@ -151,45 +151,25 @@ class HrPayslip(models.Model):
day_from = datetime.combine(fields.Date.from_string(date_from), time.min)
day_to = datetime.combine(fields.Date.from_string(date_to), time.max)
# compute leave days
leaves = {}
calendar = contract.resource_calendar_id
tz = timezone(calendar.tz)
day_leave_intervals = contract.employee_id.list_leaves(day_from, day_to, calendar=contract.resource_calendar_id)
for day, hours, leave in day_leave_intervals:
holiday = leave.holiday_id
current_leave_struct = leaves.setdefault(holiday.holiday_status_id, {
'name': holiday.holiday_status_id.name,
'sequence': 5,
'code': holiday.holiday_status_id.code or holiday.holiday_status_id.name,
'number_of_days': 0.0,
'number_of_hours': 0.0,
'contract_id': contract.id,
})
current_leave_struct['number_of_hours'] += hours
work_hours = calendar.get_work_hours_count(
tz.localize(datetime.combine(day, time.min)),
tz.localize(datetime.combine(day, time.max)),
compute_leaves=False,
)
if work_hours:
current_leave_struct['number_of_days'] += hours / work_hours
# compute worked days
work_data = contract.employee_id.get_work_days_data(day_from, day_to, calendar=contract.resource_calendar_id)
attendances = {
'name': _("Normal Working Days paid at 100%"),
'sequence': 1,
'code': 'WORK100',
'number_of_days': work_data['days'],
'number_of_hours': work_data['hours'],
'contract_id': contract.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']
if hours:
line = {
'name': benefit_type.name,
'sequence': benefit_type.sequence,
'code': benefit_type.code,
'number_of_days': hours / calendar.hours_per_day, # n_days returned by benefit_days_data doesn't make sense for extra work
'number_of_hours': hours,
'contract_id': contract.id,
}
res.append(line)
res.append(attendances)
res.extend(leaves.values())
return res
@api.model
def get_inputs(self, contracts, date_from, date_to):
res = []
+62
View File
@@ -0,0 +1,62 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields
from odoo.osv.expression import AND, OR
class ResourceCalendar(models.Model):
_inherit = 'resource.calendar'
# UI fields
normal_attendance_ids = fields.One2many(
'resource.calendar.attendance', 'calendar_id', 'Normal working Time',
domain=[('resource_id', '=', False)])
extra_attendance_ids = fields.One2many(
'resource.calendar.attendance', 'calendar_id', 'Employees working Time',
domain=[('resource_id', '!=', False)])
class ResourceCalendarAttendance(models.Model):
_inherit = 'resource.calendar.attendance'
def _default_benefit_type_id(self):
return self.env.ref('hr_payroll.benefit_type_attendance', raise_if_not_found=False)
benefit_type_id = fields.Many2one('hr.benefit.type', 'Benefit Type', default=_default_benefit_type_id)
class ResourceCalendarLeave(models.Model):
_inherit = 'resource.calendar.leaves'
benefit_type_id = fields.Many2one('hr.benefit.type', 'Benefit Type')
class ResourceMixin(models.AbstractModel):
_inherit = "resource.mixin"
def get_benefit_days_data(self, benefit_type, from_datetime, to_datetime, calendar=None):
"""
By default the resource calendar is used, but it can be
changed using the `calendar` argument.
Returns a dict {'days': n, 'hours': h} containing the number of leaves
expressed as days and as hours.
"""
resource = self.resource_id
calendar = calendar or self.resource_calendar_id
benefit_type_ids = benefit_type.ids
if benefit_type == self.env.ref('hr_payroll.benefit_type_attendance'): # special case for global attendances
benefit_type_ids += [False]# no benefit type = normal/global attendance
domain = [('benefit_type_id', 'in', benefit_type_ids)]
# naive datetimes are made explicit in UTC
from_datetime = self._timezone_datetime(from_datetime)
to_datetime = self._timezone_datetime(to_datetime)
day_total = self._get_day_total(from_datetime, to_datetime, calendar, resource)
# actual hours per day
if benefit_type.is_leave:
intervals = calendar._attendance_intervals(from_datetime, to_datetime, resource) & calendar._leave_intervals(from_datetime, to_datetime, resource, domain) # use domain to only retrieve leaves of this type
else:
intervals = calendar._attendance_intervals(from_datetime, to_datetime, resource, domain) - calendar._leave_intervals(from_datetime, to_datetime, resource)
return self._get_days_data(intervals, day_total)
@@ -33,5 +33,16 @@
<field name="groups" eval="[(4, ref('hr_payroll.group_hr_payroll_manager'))]"/>
</record>
<record id="hr_benefit_validated" model="ir.rule">
<field name="name">hr.benefit: only non validated benefits updated</field>
<field name="model_id" ref="model_hr_benefit"/>
<field name="domain_force">[('state', '!=', 'validated')]</field>
<field name="groups" eval="[(4, ref('hr_payroll.group_hr_payroll_manager'))]"/>
<field name="perm_create" eval="0"/>
<field name="perm_write" eval="1"/>
<field name="perm_unlink" eval="0"/>
<field name="perm_read" eval="0"/>
</record>
</data>
</odoo>
@@ -12,3 +12,5 @@ access_hr_rule_input_officer,hr.rule.input.office,model_hr_rule_input,hr_payroll
access_hr_salary_rule_user,hr.salary.rule.user,model_hr_salary_rule,hr_payroll.group_hr_payroll_user,1,1,1,1
access_hr_contract_advantage_template,hr.contract.advantage.template.user,model_hr_contract_advantage_template,hr_payroll.group_hr_payroll_user,1,1,1,1
access_hr_contract_advantage_template_hr_user,hr.contract.advantage.template.hr.user,model_hr_contract_advantage_template,hr.group_hr_user,1,0,0,0
access_hr_benefit,access_hr_benefit,model_hr_benefit,group_hr_payroll_user,1,1,1,1
access_hr_benefit_type,access_hr_benefit_type,model_hr_benefit_type,group_hr_payroll_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
12 access_hr_salary_rule_user hr.salary.rule.user model_hr_salary_rule hr_payroll.group_hr_payroll_user 1 1 1 1
13 access_hr_contract_advantage_template hr.contract.advantage.template.user model_hr_contract_advantage_template hr_payroll.group_hr_payroll_user 1 1 1 1
14 access_hr_contract_advantage_template_hr_user hr.contract.advantage.template.hr.user model_hr_contract_advantage_template hr.group_hr_user 1 0 0 0
15 access_hr_benefit access_hr_benefit model_hr_benefit group_hr_payroll_user 1 1 1 1
16 access_hr_benefit_type access_hr_benefit_type model_hr_benefit_type group_hr_payroll_user 1 1 1 1
+128
View File
@@ -0,0 +1,128 @@
odoo.define('hr_payroll.benefit.view_custo', function(require) {
'use strict';
var core = require('web.core');
var CalendarController = require("web.CalendarController");
var _t = core._t;
CalendarController.include({
update: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
self._renderBenefitButtons();
});
},
_renderGenerateButton: function(date_from, date_to, employee_ids, secondary) {
var self = this;
var primary = !secondary ? 'btn-primary' : 'btn-secondary';
var txt = _t("Generate Benefits");
this.$buttons.find('.o_calendar_button_month').after(
$('<button class="btn ' + primary + ' btn-benefit" type="button">'+ txt +'</button>')
.off('click')
.on('click', function (e) {
e.preventDefault();
e.stopImmediatePropagation();
var date_fmt = 'YYYY-MM-DD HH:mm:ss';
var options = {
on_close: function () {
self.reload();
},
};
self.do_action({
type: 'ir.actions.act_window',
name: txt,
res_model: 'hr.benefit.employees',
view_type: 'form',
views: [[false,'form']],
target: 'new',
context: {
'start_benefits': date_from.format(date_fmt),
'stop_benefits':date_to.format(date_fmt),
},
}, options);
})
);
},
_renderBenefitButtons: function () {
if (this.modelName !== "hr.benefit") {
return;
}
var firstDay = this.model.data.target_date.clone().startOf('month');
var lastDay = this.model.data.target_date.clone().endOf('month');
var events = this._checkDataInRange(firstDay, lastDay, this.model.data.data);
var is_validated = this._checkValidation(events);
this.$buttons.find('.btn-benefit').remove();
var employee_ids = _.map(events, function (event) { return event.record.employee_id[0]; });
employee_ids = _.uniq(employee_ids);
var self = this;
if (this.model.data.domain.length !== 0) { // select by default the employee in the domain
var employee_search_id = (this.model.data.domain[0][0] === 'employee_id' && this.model.data.domain[0][1] === '=')? [this.model.data.domain[0][2]]: null;
}
if (events.length === 0) { // Generate button
this._renderGenerateButton(firstDay, lastDay, employee_search_id);
} else {
this._renderGenerateButton(firstDay, lastDay, employee_ids, true);
}
if (is_validated && events.length !== 0) { // Generate Payslip button
this.$buttons.find('.o_calendar_button_month').after(
$('<button class="btn btn-primary btn-benefit" type="button">'+ _t('Generate Payslips') +'</button>')
.off('click')
// action_hr_payslip_by_employees
.on('click', function (e) {
e.preventDefault();
e.stopImmediatePropagation();
var date_fmt = 'YYYY-MM-DD';
self.do_action('hr_payroll.action_hr_payslip_by_employees', {
additional_context: {
default_employee_ids: employee_ids || [],
default_date_start: firstDay.format(date_fmt),
default_date_end: lastDay.format(date_fmt),
},
});
})
);
}
else if (!is_validated) { // Validate button
this.$buttons.find('.o_calendar_button_month').after(
$('<button class="btn btn-primary btn-benefit" type="button">'+ _t('Validate Benefits') +'</button>')
.off('click')
.on('click', function (e) {
e.preventDefault();
e.stopImmediatePropagation();
self._rpc({
model: 'hr.benefit',
method: 'action_validate',
args: [_.map(events, function (event) { return event.record.id; })],
}).then(function () {
return self.reload();
});
})
);
}
},
_checkDataInRange: function (firstDay, lastDay, events) {
var res = _.filter(events, function (event) {
// Filter records that are not inside the current month
// (because in calendar view some days of prev. and next month are visible)
return event.record.date_start.isBefore(lastDay) && event.record.date_stop.isAfter(firstDay);
});
return res;
},
_checkValidation: function (records) {
return _.every(records, function (record) {
return record.record.state === 'validated';
});
},
renderButtons: function () {
this._super.apply(this, arguments);
this._renderBenefitButtons();
},
});
});
+1
View File
@@ -2,3 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_payslip_flow
from . import test_benefit
+337
View File
@@ -0,0 +1,337 @@
# # -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
import pytz
from dateutil.relativedelta import relativedelta
from odoo import exceptions
from odoo.tests.common import tagged
from odoo.addons.hr_payroll.tests.common import TestPayslipBase
@tagged('benefit')
class TestBenefit(TestPayslipBase):
def setUp(self):
super(TestBenefit, self).setUp()
self.tz = pytz.timezone(self.richard_emp.tz)
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({
'date_start': self.start - relativedelta(days=5),
'name': 'dodo',
'resource_calendar_id': self.resource_calendar_id.id,
'wage': 1000,
'employee_id': self.richard_emp.id,
'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': 'Legal Leaves',
'time_type': 'leave',
'allocation_type': 'no',
'benefit_type_id': self.benefit_type_leave.id
})
self.benefit_type = self.env['hr.benefit.type'].create({
'name': 'attendance',
'is_leave': False,
'code': 'LEAVE100'
})
def test_no_duplicate(self):
self.richard_emp.generate_benefit(self.start, self.end)
pou1 = self.env['hr.benefit'].search_count([])
self.richard_emp.generate_benefit(self.start, self.end)
pou2 = self.env['hr.benefit'].search_count([])
self.assertEqual(pou1, pou2, "Benefits should not be duplicated")
def test_benefit(self):
self.richard_emp.generate_benefit(self.start, self.end)
attendance_nb = len(self.resource_calendar_id._attendance_intervals(self.start, self.end))
benefit_nb = self.env['hr.benefit'].search_count([('employee_id', '=', self.richard_emp.id)])
self.assertEqual(attendance_nb, benefit_nb, "One benefit should be generated for each calendar attendance")
def test_split_benefit_by_day(self):
start = datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz)
end = datetime.strptime('2015-11-03 18:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz)
# Benefit of type attendance should be split in three
benefit = self.env['hr.benefit'].create({
'name': '1',
'employee_id': self.richard_emp.id,
'date_start': start,
'date_stop': end,
})
benefits = benefit.split_by_day()
self.assertEqual(len(benefits), 3, "Benefit should be split in three")
self.assertEqual(pytz.utc.localize(benefits[0].date_start), self.tz.localize(datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S')))
self.assertEqual(pytz.utc.localize(benefits[0].date_stop), self.tz.localize(datetime.strptime('2015-11-01 23:59:59', '%Y-%m-%d %H:%M:%S')))
self.assertEqual(pytz.utc.localize(benefits[1].date_start), self.tz.localize(datetime.strptime('2015-11-02 00:00:00', '%Y-%m-%d %H:%M:%S')))
self.assertEqual(pytz.utc.localize(benefits[1].date_stop), self.tz.localize(datetime.strptime('2015-11-02 23:59:59', '%Y-%m-%d %H:%M:%S')))
self.assertEqual(pytz.utc.localize(benefits[2].date_start), self.tz.localize(datetime.strptime('2015-11-03 00:00:00', '%Y-%m-%d %H:%M:%S')))
self.assertEqual(pytz.utc.localize(benefits[2].date_stop), self.tz.localize(datetime.strptime('2015-11-03 18:00:00', '%Y-%m-%d %H:%M:%S')))
# Test with end at mid-night -> should not create benefit starting and ending at the same time (at 00:00)
start = datetime.strptime('2013-11-01 00:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz)
end = datetime.strptime('2013-11-04 00:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz)
benefit = self.env['hr.benefit'].create({
'name': '1',
'employee_id': self.richard_emp.id,
'date_start': start,
'date_stop': end,
})
benefits = benefit.split_by_day()
self.assertEqual(len(benefits), 3, "Benefit should be split in three")
self.assertEqual(pytz.utc.localize(benefits[0].date_start), self.tz.localize(datetime.strptime('2013-11-01 00:00:00', '%Y-%m-%d %H:%M:%S')))
self.assertEqual(pytz.utc.localize(benefits[0].date_stop), self.tz.localize(datetime.strptime('2013-11-01 23:59:59', '%Y-%m-%d %H:%M:%S')))
self.assertEqual(pytz.utc.localize(benefits[1].date_start), self.tz.localize(datetime.strptime('2013-11-02 00:00:00', '%Y-%m-%d %H:%M:%S')))
self.assertEqual(pytz.utc.localize(benefits[1].date_stop), self.tz.localize(datetime.strptime('2013-11-02 23:59:59', '%Y-%m-%d %H:%M:%S')))
self.assertEqual(pytz.utc.localize(benefits[2].date_start), self.tz.localize(datetime.strptime('2013-11-03 00:00:00', '%Y-%m-%d %H:%M:%S')))
self.assertEqual(pytz.utc.localize(benefits[2].date_stop), self.tz.localize(datetime.strptime('2013-11-03 23:59:59', '%Y-%m-%d %H:%M:%S')))
def test_duplicate_global_benefit_to_attendance(self):
start = datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz)
end = datetime.strptime('2015-11-03 18:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz)
benef = self.env['hr.benefit'].create({
'name': '1',
'employee_id': self.richard_emp.id,
'benefit_type_id': self.env.ref('hr_payroll.benefit_type_attendance').id,
'date_start': start,
'date_stop': end,
})
benef._duplicate_to_calendar()
attendance_nb = self.env['resource.calendar.attendance'].search_count([
('date_from', '>=', start.date()),
('date_to', '<=', end.date())
])
self.assertEqual(attendance_nb, 0, "It should not duplicate the 'normal/global' benefit type")
def test_duplicate_benefit_to_attendance(self):
start = datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz)
end = datetime.strptime('2015-11-03 18:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz)
# Benefit (not leave) should be split in three attendance
benef = self.env['hr.benefit'].create({
'name': '1',
'employee_id': self.richard_emp.id,
'benefit_type_id': self.benefit_type.id,
'date_start': start,
'date_stop': end,
})
benef._duplicate_to_calendar()
attendance_nb = self.env['resource.calendar.attendance'].search_count([
('date_from', '>=', start.date()),
('date_to', '<=', end.date())
])
self.assertEqual(attendance_nb, 3, "It should create one calendar attendance per day")
self.assertTrue(self.env['resource.calendar.attendance'].search([
('date_from', '=', datetime.strptime('2015-11-01', '%Y-%m-%d').date()),
('date_to', '=', datetime.strptime('2015-11-01', '%Y-%m-%d').date()),
('hour_from', '=', 9.0),
('hour_to', '>=', 23.9)
]))
self.assertTrue(self.env['resource.calendar.attendance'].search([
('date_from', '=', datetime.strptime('2015-11-02', '%Y-%m-%d').date()),
('date_to', '=', datetime.strptime('2015-11-02', '%Y-%m-%d').date()),
('hour_from', '=', 0.0),
('hour_to', '>=', 23.9)
]))
self.assertTrue(self.env['resource.calendar.attendance'].search([
('date_from', '=', datetime.strptime('2015-11-03', '%Y-%m-%d').date()),
('date_to', '=', datetime.strptime('2015-11-03', '%Y-%m-%d').date()),
('hour_from', '=', 0.0),
('hour_to', '=', 18.0)
]))
def test_create_benefit_leave(self):
start = datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz)
end = datetime.strptime('2015-11-03 18:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz)
benef = self.env['hr.benefit'].create({
'name': 'Richard leave from benef',
'employee_id': self.richard_emp.id,
'benefit_type_id': self.benefit_type_leave.id,
'date_start': start,
'date_stop': end,
})
benef.action_validate(benef.ids)
calendar_leave = self.env['resource.calendar.leaves'].search([('name', '=', 'Richard leave from benef')])
self.assertTrue(calendar_leave, "It should have created a leave in the calendar")
self.assertEqual(calendar_leave.benefit_type_id, benef.benefit_type_id, "It should have the same benefit type")
def test_validate_conflict_benefit(self):
start = datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz)
end = datetime.strptime('2015-11-01 13:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz)
benef1 = self.env['hr.benefit'].create({
'name': '1',
'employee_id': self.richard_emp.id,
'benefit_type_id': self.env.ref('hr_payroll.benefit_type_attendance').id,
'date_start': start,
'date_stop': end + relativedelta(hours=5),
})
benef2 = self.env['hr.benefit'].create({
'name': '2',
'employee_id': self.richard_emp.id,
'benefit_type_id': self.env.ref('hr_payroll.benefit_type_attendance').id,
'date_start': start + relativedelta(hours=3),
'date_stop': end,
})
self.assertFalse(benef1.action_validate(benef1.ids), "It should not validate benefits conflicting with others")
self.assertTrue(benef1.display_warning)
self.assertNotEqual(benef1.state, 'validated')
def test_validate_non_approved_leave_benefit(self):
benef1 = self.env['hr.benefit'].create({
'name': '1',
'employee_id': self.richard_emp.id,
'benefit_type_id': self.benefit_type_leave.id,
'date_start': self.start,
'date_stop': self.end,
})
leave_1 = self.env['hr.leave'].create({
'name': 'Doctor Appointment',
'employee_id': self.richard_emp.id,
'holiday_status_id': self.leave_type.id,
'date_from': self.start - relativedelta(days=1),
'date_to': self.start + relativedelta(days=1),
'number_of_days': 2,
})
self.assertFalse(benef1.action_validate(benef1.ids),"It should not validate benefits conflicting with non approved leaves")
self.assertTrue(benef1.display_warning)
def test_validate_undefined_benefit(self):
benef1 = self.env['hr.benefit'].create({
'name': '1',
'employee_id': self.richard_emp.id,
'date_start': self.start,
'date_stop': self.end,
})
self.assertFalse(benef1.action_validate(benef1.ids),"It should not validate benefits without a type")
def test_approve_leave_benefit(self):
start = datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz)
end = datetime.strptime('2015-11-03 13:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz)
leave = 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': start + relativedelta(days=1),
'number_of_days': 2,
})
benef = self.env['hr.benefit'].create({
'name': '1',
'employee_id': self.richard_emp.id,
'benefit_type_id': self.benefit_type.id,
'date_start': start,
'date_stop': end,
'leave_id': leave.id
})
leave.action_approve()
new_leave_benef = self.env['hr.benefit'].search([
('date_start', '=', datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S')),
('date_stop', '=', datetime.strptime('2015-11-02 09:00:00', '%Y-%m-%d %H:%M:%S')),
('benefit_type_id.is_leave', '=', True)
])
new_benef = self.env['hr.benefit'].search([
('date_start', '=', datetime.strptime('2015-11-02 09:00:01', '%Y-%m-%d %H:%M:%S')),
('date_stop', '=', end),
('benefit_type_id.is_leave', '=', False)
])
self.assertTrue(new_benef, "It should have created a benefit for the last two days")
self.assertTrue(new_leave_benef, "It should have created a leave benefit for the first day")
self.assertTrue(benef.action_validate((new_benef|new_leave_benef).ids), "It should be able to validate the benefits")
def test_refuse_leave_benefit(self):
start = datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz)
end = datetime.strptime('2015-11-03 13:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz)
leave = 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': start + relativedelta(days=1),
'number_of_days': 2,
})
benef = self.env['hr.benefit'].create({
'name': '1',
'employee_id': self.richard_emp.id,
'benefit_type_id': self.benefit_type.id,
'date_start': start,
'date_stop': end,
'leave_id': leave.id
})
benef.action_validate(benef.ids)
self.assertTrue(benef.display_warning, "It should have an error (conflicting leave to approve")
leave.action_refuse()
self.assertFalse(benef.display_warning, "It should not have an error")
def test_time_normal_benefit(self):
# Normal attendances (global to all employees)
data = self.richard_emp.get_benefit_days_data(self.env.ref('hr_payroll.benefit_type_attendance'), self.start, self.end)
self.assertEqual(data['hours'], 168.0)
def test_time_extra_benefit(self):
start = datetime.strptime('2015-11-01 10:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz)
end = datetime.strptime('2015-11-01 17:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz)
benef = self.env['hr.benefit'].create({
'name': '1',
'employee_id': self.richard_emp.id,
'benefit_type_id': self.benefit_type.id,
'date_start': start,
'date_stop': end,
})
benef.action_validate(benef.ids)
data = self.richard_emp.get_benefit_days_data(self.benefit_type, self.start, self.end)
self.assertEqual(data['hours'], 7.0)
def test_time_week_leave_benefit(self):
# /!\ this is a week day => it exists an calendar attendance at this time
start = datetime.strptime('2015-11-02 10:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.utc) # UTC
end = datetime.strptime('2015-11-02 17:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.utc)
leave_benef = self.env['hr.benefit'].create({
'name': '1leave',
'employee_id': self.richard_emp.id,
'benefit_type_id': self.benefit_type_leave.id,
'date_start': start,
'date_stop': end,
})
leave_benef.action_validate(leave_benef.ids)
data = self.richard_emp.get_benefit_days_data(self.benefit_type_leave, self.start, self.end)
self.assertEqual(data['hours'], 5.0, "It should equal the number of hours richard should have worked")
def test_time_weekend_leave_benefit(self):
# /!\ this is in the weekend => no calendar attendance at this time
start = datetime.strptime('2015-11-01 10:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.utc) # UTC
end = datetime.strptime('2015-11-01 17:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.utc)
leave_benef = self.env['hr.benefit'].create({
'name': '1leave',
'employee_id': self.richard_emp.id,
'benefit_type_id': self.benefit_type_leave.id,
'date_start': start,
'date_stop': end,
})
leave_benef.action_validate(leave_benef.ids)
data = self.richard_emp.get_benefit_days_data(self.benefit_type_leave, self.start, self.end)
self.assertEqual(data['hours'], 0.0, "It should equal the number of hours richard should have worked")
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_backend" inherit_id="web.assets_backend">
<xpath expr="script[last()]" position="after">
<script type="text/javascript" src="/hr_payroll/static/src/js/benefit.js"></script>
</xpath>
</template>
</odoo>
@@ -0,0 +1,165 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record model="ir.actions.server" id="generate_benefit">
<field name="name">Generate Benefit</field>
<field name="model_id" ref="hr.model_hr_employee"/>
<field name="binding_model_id" ref="hr.model_hr_employee"/>
<field name="state">code</field>
<field name="code">
if records:
records.generate_benefit()
</field>
</record>
<record id="hr_benefit_action" model="ir.actions.act_window">
<field name="name">Benefit</field>
<field name="res_model">hr.benefit</field>
<field name="view_type">form</field>
<field name="view_mode">calendar,tree,form</field>
</record>
<record id="hr_benefit_calendar" model="ir.ui.view">
<field name="name">hr.benefit.calendar</field>
<field name="model">hr.benefit</field>
<field name="arch" type="xml">
<calendar string="Benefit"
date_start="date_start"
date_stop="date_stop"
mode="month"
quick_add="False"
color="benefit_type_id"
event_limit="5">
<field name="state" invisible="1"/>
<field name="employee_id" invisible="1"/>
<field name="display_warning" invisible="1"/>
<field name="name"/>
<templates>
<t t-name="calendar-box">
<t t-set="color" t-value="widget.getColor(event.color_index)"/>
<div t-att-style="typeof color === 'string' ? ('background-color:'+color)+';' : ''" t-attf-class="#{record.is_highlighted &amp;&amp; record.is_highlighted.value ? 'o_event_hightlight' : ''} #{typeof color === 'number' ? 'o_calendar_color_'+color : ''}">
<field name="name"/>
<div class="fc-time"/>
<div class="o_fields">
<t t-if="record.display_warning">
<span class="fa fa-exclamation-triangle mr-2" aria-label="Conflict" title="error"/>
</t>
<t t-if="record.state == 'validated'">
<span class="fa fa-check-circle-o" aria-label="Validated" title="validated"/>
<t t-esc="' '"/>
</t>
<t t-foreach="widget.displayFields" t-as="name">
<t t-esc="format(record, name)"/>
</t>
</div>
</div>
</t>
</templates>
</calendar>
</field>
</record>
<record id="hr_benefit_form" model="ir.ui.view">
<field name="name">hr.benefit.form</field>
<field name="model">hr.benefit</field>
<field name="arch" type="xml">
<form string="Benefit" >
<header>
<field name="display_warning" invisible="1"/>
<field name="leave_id" invisible="1"/>
<button name="action_cancel" states="confirmed" string="Cancel" type="object"/>
<button name="action_draft" states="cancelled" string="Reset to Draft" type="object"/>
<button string="Go To Conflicting Leave" name="action_leave" type="object" attrs="{'invisible': ['|', '!',('display_warning', '=', True), '!', ('leave_id', '!=', False)]}"/>
<field name="state" widget="statusbar"/>
</header>
<sheet>
<div attrs="{'invisible': [('display_warning', '=', False)]}">
<div class="alert alert-warning" role="alert" attrs="{'invisible': ['!', ('benefit_type_id', '=', False)]}">
This benefit cannot be validated. The benefit type is undefined.
</div>
<div attrs="{'invisible': [('benefit_type_id', '=', False)]}">
<div class="alert alert-warning" role="alert" attrs="{'invisible': [('leave_id', '=', False)]}">
This benefit cannot be validated. There is a leave to approve (or refuse) at the same time.
</div>
<div class="alert alert-warning" role="alert" attrs="{'invisible': ['!', ('leave_id', '=', False)]}">
This benefit cannot be validated. It is conflicting with at least one benefit. <br/>
Two benefits of the same employee cannot overlap at the same time.
</div>
</div>
</div>
<div class="oe_title">
<h1>
<field name="name" placeholder="Benefit Name" attrs="{'readonly': [('state', '=', 'validated')]}"/>
</h1>
</div>
<group>
<group>
<field name="employee_id" attrs="{'readonly': [('state', '!=', 'draft')]}" />
<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>
<group>
<field name="date_start" attrs="{'readonly': [('state', '!=', 'draft')]}" />
<field name="date_stop" attrs="{'readonly': [('state', '!=', 'draft')]}" />
<field name="duration" attrs="{'readonly': [('state', '!=', 'draft')]}" />
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_hr_benefit_filter" model="ir.ui.view">
<field name="name">hr.benefit.filter</field>
<field name="model">hr.benefit</field>
<field name="arch" type="xml">
<search string="Search Benefit">
<field name="employee_id"/>
<field name="name"/>
<filter string="My Benefits" domain="[('employee_id.user_id','=',uid)]" name="my_benefits"/>
<filter name="group_employee" string="Employee" context="{'group_by':'employee_id'}"/>
</search>
</field>
</record>
<record id="hr_benefit_type_form" model="ir.ui.view">
<field name="name">hr.benefit.type.form</field>
<field name="model">hr.benefit.type</field>
<field name="arch" type="xml">
<form string="Benefit Type" >
<sheet>
<div class="oe_title">
<h1>
<field name="name" placeholder="Benefit Type Name"/>
</h1>
</div>
<group>
<group>
<field name="code"/>
</group>
<group>
<field name="is_leave"/>
<field name="sequence"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<menuitem
id="hr_menu_benefit_menu" name="Benefits" action="hr_benefit_action" parent="menu_hr_payroll_root" sequence="25"/>
<record id="action_view_hr_benefit_type" model="ir.actions.act_window">
<field name="name">Benefit Types</field>
<field name="res_model">hr.benefit.type</field>
<field name="view_mode">tree,kanban,form</field>
</record>
<menuitem
id="menu_hr_benefit_type_view"
action="action_view_hr_benefit_type"
parent="menu_hr_payroll_configuration"
sequence="25"
/>
</odoo>
@@ -0,0 +1,16 @@
<?xml version='1.0' encoding='UTF-8' ?>
<odoo>
<!-- Benefit type on hr leave -->
<record id="benefit_type_leave_form_inherit" model="ir.ui.view">
<field name="name">benefit.type.leave.form.inherit</field>
<field name="model">hr.leave.type</field>
<field name="inherit_id" ref="hr_holidays.edit_holiday_status_form"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='calendar']" position="after">
<group name="benefit" string="Benefits">
<field name="benefit_type_id"/>
</group>
</xpath>
</field>
</record>
</odoo>
+8 -1
View File
@@ -385,7 +385,14 @@
<field name="credit_note"/>
</group>
<separator string="Payslips"/>
<field name="slip_ids"/>
<field name="slip_ids">
<tree>
<field name="number"/>
<field name="employee_id"/>
<field name="name"/>
<field name="state"/>
</tree>
</field>
</sheet>
</form>
</field>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Calendar View -->
<record id="payroll_resource_calendar_view_form" model="ir.ui.view">
<field name="name">payroll.resource.calendar.view.form</field>
<field name="model">resource.calendar</field>
<field name="inherit_id" ref="resource.resource_calendar_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='attendance_ids']" position="replace">
<field name="normal_attendance_ids"/>
</xpath>
<xpath expr="//notebook" position="inside">
<page string="Employees Working Hours">
<field name="extra_attendance_ids">
<tree string="Working Time" editable="top">
<field name="resource_id" string="Employee"/>
<field name="name"/>
<field name="dayofweek"/>
<field name="hour_from" widget="float_time"/>
<field name="hour_to" widget="float_time"/>
<field name="date_from"/>
<field name="date_to"/>
<field name="day_period"/>
</tree>
</field>
</page>
</xpath>
</field>
</record>
</odoo>
+1
View File
@@ -3,3 +3,4 @@
from . import hr_payroll_payslips_by_employees
from . import hr_payroll_contribution_register_report
from . import hr_benefit_employee
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class HrPayslipEmployees(models.TransientModel):
_name = 'hr.benefit.employees'
_description = 'Generate benefits for all selected employees'
def _get_domain(self):
return [('contract_ids.state', 'in', ('open', 'pending')), ('company_id', '=', self.env.user.company_id.id)]
employee_ids = fields.Many2many('hr.employee', string='Employees', domain=lambda self: self._get_domain())
def generate_benefit(self):
if not self.employee_ids:
raise UserError(_("You must select employee(s) to generate benefits."))
date_start = self.env.context.get('start_benefits')
date_stop = self.env.context.get('stop_benefits')
date_start = fields.Datetime.from_string(date_start)
date_stop = fields.Datetime.from_string(date_stop)
self.employee_ids.generate_benefit(date_start, date_stop)
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_hr_benefit_by_employees" model="ir.ui.view">
<field name="name">hr_benefit_employees</field>
<field name="model">hr.benefit.employees</field>
<field name="arch" type="xml">
<form string="Benefits by Employees">
<header>
<button icon="fa-cogs" string="Generate" name="generate_benefit" type="object" class="oe_highlight" />
</header>
<group>
<span colspan="4" nolabel="1">This wizard will generate benefits for all selected employee(s) based on attendances.</span>
</group>
<group colspan="4" >
<separator string="Employees" colspan="4"/>
<newline/>
<field name="employee_ids" nolabel="1"/>
</group>
<footer></footer>
</form>
</field>
</record>
</odoo>
@@ -3,17 +3,22 @@
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from datetime import datetime
class HrPayslipEmployees(models.TransientModel):
_name = 'hr.payslip.employees'
_description = 'Generate payslips for all selected employees'
employee_ids = fields.Many2many('hr.employee', 'hr_employee_group_rel', 'payslip_id', 'employee_id', 'Employees',
domain=lambda self: self._get_available_contracts_domain())
def _get_available_contracts_domain(self):
return [('contract_ids.state', 'in', ('open', 'pending'))]
return [('contract_ids.state', 'in', ('open', 'pending')), ('company_id', '=', self.env.user.company_id.id)]
def _get_employees(self):
return self.env['hr.employee'].search(self._get_available_contracts_domain())
employee_ids = fields.Many2many('hr.employee', 'hr_employee_group_rel', 'payslip_id', 'employee_id', 'Employees',
domain=lambda self: self._get_available_contracts_domain(),
default=lambda self: self._get_employees())
@api.multi
@@ -23,8 +28,20 @@ class HrPayslipEmployees(models.TransientModel):
active_id = self.env.context.get('active_id')
if active_id:
[run_data] = self.env['hr.payslip.run'].browse(active_id).read(['date_start', 'date_end', 'credit_note'])
from_date = run_data.get('date_start')
to_date = run_data.get('date_end')
from_date = run_data.get('date_start')
to_date = run_data.get('date_end')
else:
from_date = datetime.strptime(self.env.context.get('default_date_start'), '%Y-%m-%d')
to_date = datetime.strptime(self.env.context.get('default_date_end'), '%Y-%m-%d')
payslip_run = self.env['hr.payslip.run'].create({
'name': from_date.strftime('%B %Y'),
'date_start': from_date,
'date_end': to_date,
})
active_id = payslip_run.id
[run_data] = payslip_run.read(['date_start', 'date_end', 'credit_note'])
if not data['employee_ids']:
raise UserError(_("You must select employee(s) to generate payslip(s)."))
for employee in self.env['hr.employee'].browse(data['employee_ids']):
@@ -44,4 +61,10 @@ class HrPayslipEmployees(models.TransientModel):
}
payslips += self.env['hr.payslip'].create(res)
payslips.compute_sheet()
return {'type': 'ir.actions.act_window_close'}
return {'type': 'ir.actions.act_window',
'res_model': 'hr.payslip.run',
'view_type': 'form',
'views': [[False, 'form']],
'res_id': active_id,
}