[MERGE] hr_payroll: Introduce hr.benefit model

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.

Additional development
==================

Most of the time we don't what is the next action that should be done when creating payslips or payslip batches.

- Improve the layout, the buttons of the payroll object.
- Refactor a little the code to reuse existing chunks of code instead of reinventing the wheel in several methods.
- Remove the hr.rule.input model.
- Remove an useless report.

See each individual commit for more information.

TaskID: 1912944, 1903239

closes odoo/odoo#29065
This commit is contained in:
Robot Odoo
2018-11-30 13:27:01 +01:00
committed by GitHub
41 changed files with 1502 additions and 597 deletions
+6 -1
View File
@@ -27,7 +27,12 @@
'views/res_config_settings_views.xml',
'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>
+68 -23
View File
@@ -82,29 +82,6 @@
<field name="sequence" eval="16"/>
</record>
<record id="hr_salary_rule_sales_commission" model="hr.salary.rule">
<field name="amount_select">code</field>
<field name="code">SALE</field>
<field name="category_id" ref="hr_payroll.ALW"/>
<field name="name">Get 1% of sales</field>
<field name="sequence" eval="17"/>
<field name="amount_python_compute">result = ((inputs.SALEURO and inputs.SALEURO.amount) + (inputs.SALASIA and inputs.SALASIA.amount)) * 0.01</field>
</record>
<!-- Rule Inputs -->
<record id="hr_rule_input_sale_a" model="hr.rule.input">
<field name="code">SALEURO</field>
<field name="name">Sales to Europe</field>
<field name="input_id" ref="hr_salary_rule_sales_commission"/>
</record>
<record id="hr_rule_input_sale_b" model="hr.rule.input">
<field name="code">SALASIA</field>
<field name="name">Sales to Asia</field>
<field name="input_id" ref="hr_salary_rule_sales_commission"/>
</record>
<!-- Salary Structure -->
<record id="structure_001" model="hr.payroll.structure">
@@ -146,6 +123,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 +137,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
+286
View File
@@ -0,0 +1,286 @@
# -*- 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.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)
def write(self, vals):
if 'state' in vals:
if vals['state'] == 'draft':
vals['active'] = True
if vals['state'] == 'cancelled':
vals['active'] = False
return super(HrBenefit, self).write(vals)
@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']],
}
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")
+48 -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,47 @@ 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,
'state': 'confirmed',
})
new_benefits.compute_conflicts_leaves_to_approve()
+73
View File
@@ -0,0 +1,73 @@
# -*- 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,
'state': 'confirmed',
})
@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,
'state': 'confirmed',
})
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
+49 -133
View File
@@ -59,8 +59,6 @@ class HrPayslip(models.Model):
note = fields.Text(string='Internal Note', readonly=True, states={'draft': [('readonly', False)]})
contract_id = fields.Many2one('hr.contract', string='Contract', readonly=True,
states={'draft': [('readonly', False)]})
details_by_salary_rule_category = fields.One2many('hr.payslip.line',
compute='_compute_details_by_salary_rule_category', string='Details by Salary Rule Category')
credit_note = fields.Boolean(string='Credit Note', readonly=True,
states={'draft': [('readonly', False)]},
help="Indicates this payslip has a refund of another")
@@ -68,11 +66,6 @@ class HrPayslip(models.Model):
copy=False, states={'draft': [('readonly', False)]})
payslip_count = fields.Integer(compute='_compute_payslip_count', string="Payslip Computation Details")
@api.multi
def _compute_details_by_salary_rule_category(self):
for payslip in self:
payslip.details_by_salary_rule_category = payslip.mapped('line_ids').filtered(lambda line: line.category_id)
@api.multi
def _compute_payslip_count(self):
for payslip in self:
@@ -119,6 +112,19 @@ class HrPayslip(models.Model):
'context': {}
}
@api.model
def create(self, vals):
res = super(HrPayslip, self).create(vals)
if not res.payslip_run_id:
self.env['hr.payslip.run'].create({
'name': res.name,
'date_start': res.date_from,
'date_end': res.date_to,
'slip_ids': [(4, res.id)],
'state': 'close',
})
return res
@api.multi
def unlink(self):
if any(self.filtered(lambda payslip: payslip.state not in ('draft', 'cancel'))):
@@ -136,14 +142,14 @@ class HrPayslip(models.Model):
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)]
payslip.write({'line_ids': lines, 'number': number})
payslip.write({'line_ids': lines, 'number': number, 'state': 'verify'})
return True
@api.model
def get_worked_day_lines(self, contracts, date_from, date_to):
"""
@param contract: Browse record of contracts
@return: returns a list of dict containing the input that should be applied for the given contract between date_from and date_to
@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
@@ -151,63 +157,23 @@ 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,
}
res.append(line)
res.append(attendances)
res.extend(leaves.values())
return res
@api.model
def get_inputs(self, contracts, date_from, date_to):
res = []
structures = contracts.get_all_structures()
rule_ids = structures.get_all_rules()
sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
inputs = self.env['hr.salary.rule'].browse(sorted_rule_ids).mapped('input_ids')
for contract in contracts:
for input in inputs:
input_data = {
'name': input.name,
'code': input.code,
'contract_id': contract.id,
}
res += [input_data]
return res
# YTI TODO: pass recordset as argument
@api.model
@@ -289,67 +255,6 @@ class HrPayslip(models.Model):
return list(result_dict.values())
# YTI TODO To rename. This method is not really an onchange, as it is not in any view
# employee_id and contract_id could be browse records. Contract_id should be a record
@api.multi
def onchange_employee_id(self, date_from, date_to, employee_id=False, contract_id=False):
#defaults
res = {
'value': {
'line_ids': [],
#delete old input lines
'input_line_ids': [(2, x,) for x in self.input_line_ids.ids],
#delete old worked days lines
'worked_days_line_ids': [(2, x,) for x in self.worked_days_line_ids.ids],
#'details_by_salary_head':[], TODO put me back
'name': '',
'contract_id': False,
'struct_id': False,
}
}
if (not employee_id) or (not date_from) or (not date_to):
return res
ttyme = datetime.combine(fields.Date.from_string(date_from), time.min)
employee = self.env['hr.employee'].browse(employee_id)
locale = self.env.context.get('lang') or 'en_US'
res['value'].update({
'name': _('Salary Slip of %s for %s') % (employee.name, tools.ustr(babel.dates.format_date(date=ttyme, format='MMMM-y', locale=locale))),
'company_id': employee.company_id.id,
})
contracts = self.env['hr.contract']
if not self.env.context.get('contract'):
#fill with the first contract of the employee
contracts = employee._get_contracts(date_from, date_to)
else:
if contract_id:
#set the list of contract for which the input have to be filled
contracts = self.env['hr.contract'].browse(contract_id)
else:
#if we don't give the contract, then the input to fill should be for all current contracts of the employee
contracts = employee._get_contracts(date_from, date_to)
if not contracts:
return res
contract = contracts[0]
res['value'].update({
'contract_id': contract.id
})
struct = contract.struct_id
if not struct:
return res
res['value'].update({
'struct_id': struct.id,
})
#computation of the salary input
worked_days_line_ids = self.get_worked_day_lines(contracts, date_from, date_to)
input_line_ids = self.get_inputs(contracts, date_from, date_to)
res['value'].update({
'worked_days_line_ids': worked_days_line_ids,
'input_line_ids': input_line_ids,
})
return res
@api.onchange('employee_id', 'date_from', 'date_to')
def onchange_employee(self):
@@ -376,18 +281,13 @@ class HrPayslip(models.Model):
return
self.struct_id = self.contract_id.struct_id
#computation of the salary input
#computation of the salary worked days
worked_days_line_ids = self.get_worked_day_lines(contracts, 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)
self.worked_days_line_ids = worked_days_lines
input_line_ids = self.get_inputs(contracts, date_from, date_to)
input_lines = self.input_line_ids.browse([])
for r in input_line_ids:
input_lines += input_lines.new(r)
self.input_line_ids = input_lines
return
@api.onchange('contract_id')
@@ -431,7 +331,7 @@ class HrPayslipLine(models.Model):
amount_fix = fields.Float(related='salary_rule_id.amount_fix', readonly=True)
amount_percentage = fields.Float(related='salary_rule_id.amount_percentage', readonly=True)
appears_on_payslip = fields.Boolean(related='salary_rule_id.appears_on_payslip', readonly=True)
category_id = fields.Many2one(related='salary_rule_id.category_id', readonly=True)
category_id = fields.Many2one(related='salary_rule_id.category_id', readonly=True, store=True)
register_id = fields.Many2one(related='salary_rule_id.register_id', readonly=True, store=True)
@api.depends('quantity', 'amount', 'rate')
@@ -462,8 +362,8 @@ class HrPayslipWorkedDays(models.Model):
code = fields.Char(required=True, help="The code that can be used in the salary rules")
number_of_days = fields.Float(string='Number of Days')
number_of_hours = fields.Float(string='Number of Hours')
contract_id = fields.Many2one('hr.contract', string='Contract', required=True,
help="The contract for which applied this input")
contract_id = fields.Many2one(related='payslip_id.contract_id', string='Contract', required=True,
help="The contract for which applied this worked days")
class HrPayslipInput(models.Model):
@@ -478,7 +378,7 @@ class HrPayslipInput(models.Model):
amount = fields.Float(help="It is used in computation. For e.g. A rule for sales having "
"1% commission of basic salary for per product can defined in expression "
"like result = inputs.SALEURO.amount * contract.wage*0.01.")
contract_id = fields.Many2one('hr.contract', string='Contract', required=True,
contract_id = fields.Many2one(related='payslip_id.contract_id', string='Contract', required=True,
help="The contract for which applied this input")
@@ -491,7 +391,8 @@ class HrPayslipRun(models.Model):
states={'draft': [('readonly', False)]})
state = fields.Selection([
('draft', 'Draft'),
('close', 'Close'),
('verify', 'Verify'),
('close', 'Done'),
], string='Status', index=True, readonly=True, copy=False, default='draft')
date_start = fields.Date(string='Date From', required=True, readonly=True,
states={'draft': [('readonly', False)]}, default=lambda self: fields.Date.to_string(date.today().replace(day=1)))
@@ -501,6 +402,11 @@ class HrPayslipRun(models.Model):
credit_note = fields.Boolean(string='Credit Note', readonly=True,
states={'draft': [('readonly', False)]},
help="If its checked, indicates that all payslips generated from here are refund payslips.")
payslip_count = fields.Integer(compute='_compute_payslip_count')
def _compute_payslip_count(self):
for payslip_run in self:
payslip_run.payslip_count = len(self.slip_ids)
@api.multi
def draft_payslip_run(self):
@@ -510,3 +416,13 @@ class HrPayslipRun(models.Model):
def close_payslip_run(self):
self.mapped('slip_ids').filtered(lambda payslip: payslip.state != 'done').action_payslip_done()
return self.write({'state': 'close'})
def action_open_payslips(self):
self.ensure_one()
return {
"type": "ir.actions.act_window",
"res_model": "hr.payslip",
"views": [[False, "tree"], [False, "form"]],
"domain": [['id', 'in', self.slip_ids.ids]],
"name": "Payslips",
}
+1 -10
View File
@@ -135,7 +135,7 @@ class HrSalaryRule(models.Model):
# rules: object containing the rules code (previously computed)
# categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category).
# worked_days: object containing the computed worked days
# inputs: object containing the computed inputs
# inputs: object containing the computed inputs.
# Note: returned value have to be set in the variable 'result'
@@ -170,7 +170,6 @@ class HrSalaryRule(models.Model):
child_ids = fields.One2many('hr.salary.rule', 'parent_rule_id', string='Child Salary Rule', copy=True)
register_id = fields.Many2one('hr.contribution.register', string='Contribution Register',
help="Eventual third party involved in the salary payment of the employees.")
input_ids = fields.One2many('hr.rule.input', 'input_id', string='Inputs', copy=True)
note = fields.Text(string='Description')
@api.constrains('parent_rule_id')
@@ -239,11 +238,3 @@ class HrSalaryRule(models.Model):
except:
raise UserError(_('Wrong python condition defined for salary rule %s (%s).') % (self.name, self.code))
class HrRuleInput(models.Model):
_name = 'hr.rule.input'
_description = 'Salary Rule Input'
name = fields.Char(string='Description', required=True)
code = fields.Char(required=True, help="The code that can be used in the salary rules")
input_id = fields.Many2one('hr.salary.rule', string='Salary Rule Input', required=True)
+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)
-1
View File
@@ -2,5 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import report_payslip_details
from . import report_contribution_register
@@ -1,99 +0,0 @@
#-*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class PayslipDetailsReport(models.AbstractModel):
_name = 'report.hr_payroll.report_payslipdetails'
_description = 'Payslip Details Report'
def get_details_by_rule_category(self, payslip_lines):
PayslipLine = self.env['hr.payslip.line']
RuleCateg = self.env['hr.salary.rule.category']
def get_recursive_parent(current_rule_category, rule_categories=None):
if rule_categories:
rule_categories = current_rule_category | rule_categories
else:
rule_categories = current_rule_category
if current_rule_category.parent_id:
return get_recursive_parent(current_rule_category.parent_id, rule_categories)
else:
return rule_categories
res = {}
result = {}
if payslip_lines:
self.env.cr.execute("""
SELECT pl.id, pl.category_id, pl.slip_id FROM hr_payslip_line as pl
LEFT JOIN hr_salary_rule_category AS rc on (pl.category_id = rc.id)
WHERE pl.id in %s
GROUP BY rc.parent_id, pl.sequence, pl.id, pl.category_id
ORDER BY pl.sequence, rc.parent_id""",
(tuple(payslip_lines.ids),))
for x in self.env.cr.fetchall():
result.setdefault(x[2], {})
result[x[2]].setdefault(x[1], [])
result[x[2]][x[1]].append(x[0])
for payslip_id, lines_dict in result.items():
res.setdefault(payslip_id, [])
for rule_categ_id, line_ids in lines_dict.items():
rule_categories = RuleCateg.browse(rule_categ_id)
lines = PayslipLine.browse(line_ids)
level = 0
for parent in get_recursive_parent(rule_categories):
res[payslip_id].append({
'rule_category': parent.name,
'name': parent.name,
'code': parent.code,
'level': level,
'total': sum(lines.mapped('total')),
})
level += 1
for line in lines:
res[payslip_id].append({
'rule_category': line.name,
'name': line.name,
'code': line.code,
'total': line.total,
'level': level
})
return res
def get_lines_by_contribution_register(self, payslip_lines):
result = {}
res = {}
for line in payslip_lines.filtered('register_id'):
result.setdefault(line.slip_id.id, {})
result[line.slip_id.id].setdefault(line.register_id, line)
result[line.slip_id.id][line.register_id] |= line
for payslip_id, lines_dict in result.items():
res.setdefault(payslip_id, [])
for register, lines in lines_dict.items():
res[payslip_id].append({
'register_name': register.name,
'total': sum(lines.mapped('total')),
})
for line in lines:
res[payslip_id].append({
'name': line.name,
'code': line.code,
'quantity': line.quantity,
'amount': line.amount,
'total': line.total,
})
return res
@api.model
def _get_report_values(self, docids, data=None):
payslips = self.env['hr.payslip'].browse(docids)
return {
'doc_ids': docids,
'doc_model': 'hr.payslip',
'docs': payslips,
'data': data,
'get_details_by_rule_category': self.get_details_by_rule_category(payslips.mapped('details_by_salary_rule_category').filtered(lambda r: r.appears_on_payslip)),
'get_lines_by_contribution_register': self.get_lines_by_contribution_register(payslips.mapped('line_ids').filtered(lambda r: r.appears_on_payslip)),
}
@@ -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>
@@ -8,7 +8,8 @@ access_hr_payslip_line,hr.payslip.line,model_hr_payslip_line,hr_payroll.group_hr
access_hr_payslip_input_user,hr.payslip.input.user,model_hr_payslip_input,hr_payroll.group_hr_payroll_user,1,1,1,1
access_hr_payslip_worked_days_officer,hr.payslip.worked_days.officer,model_hr_payslip_worked_days,hr_payroll.group_hr_payroll_user,1,1,1,1
access_hr_payslip_run,hr.payslip.run,model_hr_payslip_run,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_hr_rule_input_officer,hr.rule.input.office,model_hr_rule_input,hr_payroll.group_hr_payroll_user,1,1,1,1
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
8 access_hr_payslip_input_user hr.payslip.input.user model_hr_payslip_input hr_payroll.group_hr_payroll_user 1 1 1 1
9 access_hr_payslip_worked_days_officer hr.payslip.worked_days.officer model_hr_payslip_worked_days hr_payroll.group_hr_payroll_user 1 1 1 1
10 access_hr_payslip_run hr.payslip.run model_hr_payslip_run hr_payroll.group_hr_payroll_manager 1 1 1 1
access_hr_rule_input_officer hr.rule.input.office model_hr_rule_input hr_payroll.group_hr_payroll_user 1 1 1 1
11 access_hr_salary_rule_user hr.salary.rule.user model_hr_salary_rule hr_payroll.group_hr_payroll_user 1 1 1 1
12 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
13 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
14 access_hr_benefit access_hr_benefit model_hr_benefit group_hr_payroll_user 1 1 1 1
15 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
+1 -2
View File
@@ -18,7 +18,6 @@ class TestPayslipBase(TransactionCase):
self.prof_tax_rule_id = self.ref('hr_payroll.hr_salary_rule_professionaltax1')
self.pf_rule_id = self.ref('hr_payroll.hr_salary_rule_providentfund1')
self.mv_rule_id = self.ref('hr_payroll.hr_salary_rule_meal_voucher')
self.comm_rule_id = self.ref('hr_payroll.hr_salary_rule_sales_commission')
# I create a new employee "Richard"
self.richard_emp = self.env['hr.employee'].create({
@@ -36,7 +35,7 @@ class TestPayslipBase(TransactionCase):
'company_id': self.ref('base.main_company'),
'rule_ids': [(4, self.hra_rule_id), (4, self.conv_rule_id),
(4, self.prof_tax_rule_id), (4, self.pf_rule_id),
(4, self.mv_rule_id), (4, self.comm_rule_id)],
(4, self.mv_rule_id)],
})
# I create a contract for "Richard"
+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")
@@ -71,9 +71,6 @@ class TestPayslipFlow(TestPayslipBase):
# I print the payslip report
data, data_format = self.env.ref('hr_payroll.action_report_payslip').render(richard_payslip.ids)
# I print the payslip details report
data, data_format = self.env.ref('hr_payroll.payslip_details_report').render(richard_payslip.ids)
# I print the contribution register report
context = {'model': 'hr.contribution.register', 'active_ids': [self.ref('hr_payroll.hr_houserent_register')]}
test_reports.try_report_action(self.env.cr, self.env.uid, 'action_payslip_lines_contribution_register', context=context, our_module='hr_payroll')
@@ -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,163 @@
<?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 string="Go To Conflicting Leave" name="action_leave" type="object" attrs="{'invisible': ['|', '!',('display_warning', '=', True), '!', ('leave_id', '!=', False)]}"/>
<field name="state" widget="statusbar" options="{'clickable': '1'}"/>
</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>
@@ -19,13 +19,4 @@
print_report_name="'Payslip - %s' % (object.employee_id.name)"
/>
<report
id="payslip_details_report"
model="hr.payslip"
string="PaySlip Details"
report_type="qweb-pdf"
name="hr_payroll.report_payslipdetails"
file="hr_payroll.report_payslipdetails"
print_report_name="'Payslip Details - %s' % (object.employee_id.name)"
/>
</odoo>
+24 -25
View File
@@ -128,12 +128,12 @@
<field name="arch" type="xml">
<form string="Payslip">
<header>
<button string="Confirm" name="action_payslip_done" type="object" states="draft" class="oe_highlight"/>
<button string="Confirm" name="action_payslip_done" type="object" states="verify" class="oe_highlight"/>
<button string="Refund" name="refund_sheet" states="confirm,done" type='object' />
<button string="Set to Draft" name="action_payslip_draft" type="object" states="cancel"/>
<button string="Compute Sheet" name="compute_sheet" type="object" states="draft" class="oe_highlight"/>
<button string="Cancel Payslip" name="action_payslip_cancel" type="object" states="draft,hr_check,confirm,verify"/>
<field name="state" widget="statusbar" statusbar_visible="draft,confirm"/>
<button string="Cancel Payslip" name="action_payslip_cancel" type="object" states="draft,confirm,verify"/>
<field name="state" widget="statusbar" statusbar_visible="draft,verify,confirm"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
@@ -157,7 +157,7 @@
<field name="number"/>
<field name="struct_id" attrs="{'required':[('contract_id','!=',False)]}"/>
<field name="name"/>
<field name="credit_note"/>
<field name="credit_note" invisible="1"/>
</group>
<notebook>
<page string="Worked Days &amp; Inputs">
@@ -168,7 +168,7 @@
<field name="code"/>
<field name="number_of_days" sum="Total Working Days"/>
<field name="number_of_hours"/>
<field name="contract_id"/>
<field name="contract_id" invisible="True"/>
<field name="sequence" invisible="True"/>
</tree>
<form string="Worked Day">
@@ -188,7 +188,7 @@
<field name="name"/>
<field name="code"/>
<field name="amount"/>
<field name="contract_id"/>
<field name="contract_id" invisible="True"/>
<field name="sequence" invisible="True"/>
</tree>
<form string="Payslip Line">
@@ -230,17 +230,7 @@
</form>
</field>
</page>
<page string="Details By Salary Rule Category">
<field name="details_by_salary_rule_category" context="{'group_by':'category_id'}" domain="[('appears_on_payslip', '=', True)]">
<tree string="Payslip Lines" decoration-info="total == 0">
<field name="category_id"/>
<field name="name"/>
<field name="code"/>
<field name="total"/>
</tree>
</field>
</page>
<page string="Accounting Information">
<page string="Accounting Information">
<group>
<group string="Miscellaneous">
<field name="company_id" groups="base.group_multi_company"/>
@@ -253,7 +243,7 @@
<div colspan="4">
<field name="note" placeholder="Add an internal note..."/>
</div>
</page>
</page>
</notebook>
</sheet>
</form>
@@ -265,15 +255,17 @@
<field name="model">hr.payslip</field>
<field name="arch" type="xml">
<search string="Search Payslips">
<field name="employee_id"/>
<field name="name" string="Payslips" filter_domain="['|',('name','ilike',self),('number','ilike',self)]"/>
<field name="date_from"/>
<field name="payslip_run_id"/>
<filter string="Draft" name="draft" domain="[('state','=','draft')]" help="Draft Slip"/>
<filter string="Done" name="done" domain="[('state','=','done')]" help="Done Slip"/>
<field name="employee_id"/>
<field name="payslip_run_id"/>
<separator/>
<filter string="Credit Notes" name="credit_note" domain="[('credit_note', '=', True)]"/>
<group expand="0" string="Group By">
<filter string="Employees" name="employee_id" context="{'group_by':'employee_id'}"/>
<filter string="PaySlip Batch" name="payslip_run_id" context="{'group_by':'payslip_run_id'}"/>
<filter string="PaySlip Batch" name="group_by_batch" context="{'group_by':'payslip_run_id'}"/>
<filter string="Companies" name="company_id" groups="base.group_multi_company" context="{'group_by':'company_id'}"/>
<filter string="States" name="state" context="{'group_by':'state'}"/>
</group>
@@ -286,6 +278,7 @@
<field name="view_type">form</field>
<field name="view_mode">tree,kanban,form</field>
<field name="search_view_id" ref="view_hr_payslip_filter"/>
<field name="context">{'search_default_group_by_batch': 1}</field>
</record>
<menuitem action="action_view_hr_payslip_form" id="menu_department_tree" parent="menu_hr_payroll_root" groups="hr_payroll.group_hr_payroll_user"/>
@@ -367,12 +360,20 @@
<field name="arch" type="xml">
<form string="Payslips Batches">
<header>
<button name="close_payslip_run" type="object" string="Close" states="draft" class="oe_highlight"/>
<button name="%(action_hr_payslip_by_employees)d" type="action" states="draft" string="Generate Payslips" class="oe_highlight"/>
<button string="Set to Draft" name="draft_payslip_run" type="object" states="close" />
<button name="close_payslip_run" type="object" string="Validate" states="verify" class="oe_highlight"/>
<button string="Set to Draft" name="draft_payslip_run" type="object" states="close"/>
<field name="state" widget="statusbar"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="action_open_payslips" class="oe_stat_button" icon="fa-book" type="object" help="Generated Payslips" attrs="{'invisible': [('payslip_count', '=', 0)]}">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value"><field name="payslip_count"/></span>
<span class="o_stat_text">Payslips</span>
</div>
</button>
</div>
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name"/>
@@ -384,8 +385,6 @@
</div>
<field name="credit_note"/>
</group>
<separator string="Payslips"/>
<field name="slip_ids"/>
</sheet>
</form>
</field>
@@ -346,14 +346,6 @@
<separator string="Children Definition"/>
<field name="child_ids"/>
</page>
<page name="inputs" string="Inputs">
<field name="input_ids" mode="tree">
<tree string="Input Data" editable="bottom">
<field name="name"/>
<field name="code"/>
</tree>
</field>
</page>
<page name="description" string="Description">
<field name="note"/>
</page>
@@ -1,101 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="report_payslipdetails">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<t t-call="web.external_layout">
<div class="page">
<h2>Pay Slip</h2>
<p t-field="o.name"/>
<table class="table table-sm table-bordered">
<tr>
<td><strong>Name</strong></td>
<td><span t-field="o.employee_id"/></td>
<td><strong>Designation</strong></td>
<td><span t-field="o.employee_id.job_id"/></td>
</tr>
<tr>
<td><strong>Address</strong></td>
<td colspan="3">
<div t-field="o.employee_id.address_home_id"
t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True, "phone_icons": True}'/>
</td>
</tr>
<tr>
<td><strong>Email</strong></td>
<td><span t-field="o.employee_id.work_email"/></td>
<td><strong>Identification No</strong></td>
<td><span t-field="o.employee_id.identification_id"/></td>
</tr>
<tr>
<td><strong>Reference</strong></td>
<td><span t-field="o.number"/></td>
<td><strong>Bank Account</strong></td>
<td><span t-field="o.employee_id.bank_account_id"/></td>
</tr>
<tr>
<td><strong>Date From</strong></td>
<td><span t-field="o.date_from"/></td>
<td><strong>Date To</strong></td>
<td><span t-field="o.date_to"/></td>
</tr>
</table>
<h3>Details by Salary Rule Category</h3>
<table class="table table-sm mb32">
<thead>
<tr>
<th>Code</th>
<th>Salary Rule Category</th>
<th>Total</th>
</tr>
</thead>
<tbody>
<tr t-foreach="get_details_by_rule_category.get(o.id, [])" t-as="h">
<td>
<span t-esc="h['code']"/>
</td>
<td>
<span t-esc="'..'*h['level']"/><span t-esc="h['rule_category']"/>
</td>
<td class="text-right">
<span t-esc="h['total']"
t-esc-options='{"widget": "monetary", "display_currency": o.company_id.currency_id}'/>
</td>
</tr>
</tbody>
</table>
<h3>Payslip Lines by Contribution Register</h3>
<table class="table table-sm mt32">
<thead>
<tr>
<th>Code</th>
<th>Name</th>
<th>Quantity/rate</th>
<th>Amount</th>
<th>Total</th>
</tr>
</thead>
<tbody>
<tr t-foreach="get_lines_by_contribution_register.get(o.id, [])" t-as="p">
<td><span t-esc="p.get('code', '')"/></td>
<td><span t-esc="p.get('name', '')"/></td>
<td><span t-esc="p.get('quantity', '')"/></td>
<td><span t-esc="p.get('amount', 0)"/></td>
<td class="text-right">
<span t-esc="p.get('total', 0)"
t-esc-options='{"widget": "monetary", "display_currency": o.company_id.currency_id}'/>
</td>
</tr>
</tbody>
</table>
<p class="text-right"><strong>Authorized signature</strong></p>
</div>
</t>
</t>
</t>
</template>
</odoo>
@@ -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,45 +3,63 @@
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(), required=True)
@api.multi
def compute_sheet(self):
payslips = self.env['hr.payslip']
[data] = self.read()
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')
if not data['employee_ids']:
self.ensure_one()
if not self.env.context.get('active_id'):
from_date = datetime.strptime(self.env.context.get('default_date_start'), '%Y-%m-%d')
end_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': end_date,
})
else:
payslip_run = self.env['hr.payslip.run'].browse(self.env.context.get('active_id'))
if not self.employee_ids:
raise UserError(_("You must select employee(s) to generate payslip(s)."))
for employee in self.env['hr.employee'].browse(data['employee_ids']):
slip_data = self.env['hr.payslip'].onchange_employee_id(from_date, to_date, employee.id, contract_id=False)
res = {
payslips = self.env['hr.payslip']
Payslip = self.env['hr.payslip']
for employee in self.employee_ids:
values = Payslip.default_get(Payslip.fields_get())
values.update({
'employee_id': employee.id,
'name': slip_data['value'].get('name'),
'struct_id': slip_data['value'].get('struct_id'),
'contract_id': slip_data['value'].get('contract_id'),
'payslip_run_id': active_id,
'input_line_ids': [(0, 0, x) for x in slip_data['value'].get('input_line_ids')],
'worked_days_line_ids': [(0, 0, x) for x in slip_data['value'].get('worked_days_line_ids')],
'date_from': from_date,
'date_to': to_date,
'credit_note': run_data.get('credit_note'),
'company_id': employee.company_id.id,
}
payslips += self.env['hr.payslip'].create(res)
'date_from': payslip_run.date_start,
'date_to': payslip_run.date_end,
'credit_note': payslip_run.credit_note,
'payslip_run_id': payslip_run.id,
})
payslip = self.env['hr.payslip'].new(values)
payslip.onchange_employee()
payslip.onchange_contract()
values = payslip._convert_to_write(payslip._cache)
payslips += Payslip.create(values)
payslips.compute_sheet()
return {'type': 'ir.actions.act_window_close'}
payslip_run.state = 'verify'
return {
'type': 'ir.actions.act_window',
'res_model': 'hr.payslip.run',
'view_type': 'form',
'views': [[False, 'form']],
'res_id': payslip_run.id,
}
@@ -6,9 +6,6 @@
<field name="model">hr.payslip.employees</field>
<field name="arch" type="xml">
<form string="Payslips by Employees">
<header>
<button icon="fa-cogs" string="Generate" name="compute_sheet" type="object" class="oe_highlight" />
</header>
<group>
<span colspan="4" nolabel="1">This wizard will generate payslips for all selected employee(s) based on the dates and credit note specified on Payslips Run.</span>
</group>
@@ -17,6 +14,10 @@
<newline/>
<field name="employee_ids" nolabel="1"/>
</group>
<footer>
<button icon="fa-cogs" string="Generate" name="compute_sheet" type="object" class="oe_highlight"/>
<button string="Cancel" class="btn btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
@@ -69,7 +69,7 @@ class HrPayslip(models.Model):
'journal_id': slip.journal_id.id,
'date': date,
}
for line in slip.details_by_salary_rule_category:
for line in slip.line_ids.filtered(lambda line: line.category_id):
amount = slip.credit_note and -line.total or line.total
if float_is_zero(amount, precision_digits=precision):
continue
@@ -56,7 +56,6 @@ class TestHrPayrollAccount(common.TransactionCase):
self.ref('hr_payroll.hr_salary_rule_professionaltax1'),
self.ref('hr_payroll.hr_salary_rule_providentfund1'),
self.ref('hr_payroll.hr_salary_rule_meal_voucher'),
self.ref('hr_payroll.hr_salary_rule_sales_commission')
])],
})
@@ -75,23 +74,16 @@ class TestHrPayrollAccount(common.TransactionCase):
self.hr_payslip = self.env['hr.payslip'].create({
'employee_id': self.hr_employee_john.id,
'journal_id': self.ref('hr_payroll_account.expenses_journal'),
'name': 'Test Payslip',
})
def test_00_hr_payslip(self):
""" checking the process of payslip. """
date_from = time.strftime('%Y-%m-01')
date_to = str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10]
res = self.hr_payslip.onchange_employee_id(date_from, date_to, self.hr_employee_john.id)
vals = {
'struct_id': res['value']['struct_id'],
'contract_id': res['value']['contract_id'],
'name': res['value']['name'],
}
vals['worked_days_line_ids'] = [(0, 0, i) for i in res['value']['worked_days_line_ids']]
vals['input_line_ids'] = [(0, 0, i) for i in res['value']['input_line_ids']]
vals.update({'contract_id': self.hr_contract_john.id})
self.hr_payslip.write(vals)
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()
# I assign the amount to Input data.
payslip_input = self.env['hr.payslip.input'].search([('payslip_id', '=', self.hr_payslip.id)])
@@ -8,7 +8,7 @@ class HrPayslipEmployees(models.TransientModel):
@api.multi
def compute_sheet(self):
journal_id = False
if self.env.context.get('active_id'):
journal_id = self.env['hr.payslip.run'].browse(self.env.context.get('active_id')).journal_id.id
return super(HrPayslipEmployees, self.with_context(journal_id=journal_id)).compute_sheet()
return super(HrPayslipEmployees, self.with_context(journal_id=journal_id)).compute_sheet()
return super(HrPayslipEmployees, self).compute_sheet()
@@ -518,65 +518,4 @@ or - (contract.wage * 0.12))</field>
<field name="appears_on_payslip" eval="False"/>
<field name="note">Both the employees and employer contribute to the fund at the rate of 12% of the basic wages, dearness allowance and retaining allowance, if any, payable to employees per month.</field>
</record>
<!-- Rule Inputs -->
<record id="hr_rule_input_performance" model="hr.rule.input">
<field name="code">PERF</field>
<field name="name">Performance of Employee</field>
<field name="input_id" ref="hr_salary_rule_performance"/>
</record>
<record id="hr_rule_input_bonus" model="hr.rule.input">
<field name="code">BNS</field>
<field name="name">Bonus of Employee</field>
<field name="input_id" ref="hr_salary_rule_bonus"/>
</record>
<record id="hr_rule_input_arrears" model="hr.rule.input">
<field name="code">ARS</field>
<field name="name">Arrears</field>
<field name="input_id" ref="hr_salary_rule_arrears"/>
</record>
<record id="hr_rule_input_le" model="hr.rule.input">
<field name="code">LE</field>
<field name="name">Leave Encashment</field>
<field name="input_id" ref="hr_salary_rule_le"/>
</record>
<record id="hr_rule_input_journals" model="hr.rule.input">
<field name="code">PJ</field>
<field name="name">Periodical Journals</field>
<field name="input_id" ref="hr_salary_rule_journals"/>
</record>
<record id="hr_rule_input_telephone" model="hr.rule.input">
<field name="code">TR</field>
<field name="name">Telephone Reimbursement</field>
<field name="input_id" ref="hr_salary_rule_telephone"/>
</record>
<record id="hr_rule_input_car" model="hr.rule.input">
<field name="code">CEI</field>
<field name="name">Car Expenses</field>
<field name="input_id" ref="hr_payroll_rule_car"/>
</record>
<record id="hr_rule_input_internet" model="hr.rule.input">
<field name="code">IE</field>
<field name="name">Internet Expence</field>
<field name="input_id" ref="hr_salary_rule_internet"/>
</record>
<record id="hr_rule_input_leave_avail" model="hr.rule.input">
<field name="code">LAI</field>
<field name="name">Leave Availed</field>
<field name="input_id" ref="hr_payslip_rule_dla"/>
</record>
<record id="hr_rule_input_medical" model="hr.rule.input">
<field name="code">MR</field>
<field name="name">Medical Reimbursement</field>
<field name="input_id" ref="hr_salary_rule_medical"/>
</record>
</odoo>
@@ -1,7 +1,6 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import report_payslip_details
from . import report_payroll_advice
from . import report_hr_salary_employee_bymonth
from . import payment_advice_report
@@ -1,20 +0,0 @@
#-*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class PayslipDetailsReportIN(models.AbstractModel):
_name = 'report.l10n_in_hr_payroll.report_payslipdetails'
_inherit = 'report.hr_payroll.report_payslipdetails'
@api.model
def _get_report_values(self, docids, data=None):
payslips = self.env['hr.payslip'].browse(docids)
return {
'doc_ids': docids,
'doc_model': 'hr.payslip',
'docs': payslips,
'data': data,
'get_details_by_rule_category': self.get_details_by_rule_category(payslips.mapped('details_by_salary_rule_category'))
}
@@ -21,7 +21,7 @@ class TestPaymentAdviceBatch(TestPaymentAdviceBase):
payslip_employee.with_context(active_id=payslip_run.id).compute_sheet()
# I check that the Payslip Batch is in "Draft"
self.assertEqual(payslip_run.state, 'draft')
self.assertEqual(payslip_run.state, 'verify')
# Now I close Payslip Batch
payslip_run.write({'state': 'close'})
+10 -2
View File
@@ -197,13 +197,20 @@ class ResourceCalendar(models.Model):
# --------------------------------------------------
# Computation API
# --------------------------------------------------
def _attendance_intervals(self, start_dt, end_dt, resource=None):
def _attendance_intervals(self, start_dt, end_dt, resource=None, domain=None):
""" Return the attendance intervals in the given datetime range.
The returned intervals are expressed in the resource's timezone.
"""
assert start_dt.tzinfo and end_dt.tzinfo
combine = datetime.combine
resource_ids = [resource.id, False] if resource else [False]
domain = domain if domain is not None else []
domain = domain + [
('calendar_id', '=', self.id),
('resource_id', 'in', resource_ids),
]
# express all dates and times in the resource's timezone
tz = timezone((resource or self).tz)
start_dt = start_dt.astimezone(tz)
@@ -211,7 +218,7 @@ class ResourceCalendar(models.Model):
# for each attendance spec, generate the intervals in the date range
result = []
for attendance in self.attendance_ids:
for attendance in self.env['resource.calendar.attendance'].search(domain):
start = start_dt.date()
if attendance.date_from:
start = max(start, attendance.date_from)
@@ -404,6 +411,7 @@ class ResourceCalendarAttendance(models.Model):
hour_to = fields.Float(string='Work to', required=True)
calendar_id = fields.Many2one("resource.calendar", string="Resource's Calendar", required=True, ondelete='cascade')
day_period = fields.Selection([('morning', 'Morning'), ('afternoon', 'Afternoon')], required=True, default='morning')
resource_id = fields.Many2one('resource.resource', 'Resource')
@api.onchange('hour_from', 'hour_to')
def _onchange_hours(self):
+50 -48
View File
@@ -53,6 +53,47 @@ class ResourceMixin(models.AbstractModel):
default['resource_calendar_id'] = resource.calendar_id.id
return super(ResourceMixin, self).copy_data(default)
def _get_days_data(self, intervals, day_total):
"""
helper function to compute duration of `intervals`
expressed in days and hours.
`day_total` is a dict {date: n_hours} with the number of hours for each day.
"""
day_hours = defaultdict(float)
for start, stop, meta in intervals:
day_hours[start.date()] += (stop - start).total_seconds() / 3600
# compute number of days as quarters
days = sum(
float_utils.round(ROUNDING_FACTOR * day_hours[day] / day_total[day]) / ROUNDING_FACTOR
for day in day_hours
)
return {
'days': days,
'hours': sum(day_hours.values()),
}
def _get_day_total(self, from_datetime, to_datetime, calendar, resource):
"""
@return dict with hours of attendance in each day between `from_datetime` and `to_datetime`
"""
# total hours per day: retrieve attendances with one extra day margin,
# in order to compute the total hours on the first and last days
from_full = from_datetime - timedelta(days=1)
to_full = to_datetime + timedelta(days=1)
intervals = calendar._attendance_intervals(from_full, to_full, resource)
day_total = defaultdict(float)
for start, stop, meta in intervals:
day_total[start.date()] += (stop - start).total_seconds() / 3600
return day_total
def _timezone_datetime(self, time):
if not time.tzinfo:
time = time.replace(tzinfo=utc)
return time
def get_work_days_data(self, from_datetime, to_datetime, compute_leaves=True, calendar=None, domain=None):
"""
By default the resource calendar is used, but it can be
@@ -68,38 +109,18 @@ class ResourceMixin(models.AbstractModel):
calendar = calendar or self.resource_calendar_id
# naive datetimes are made explicit in UTC
if not from_datetime.tzinfo:
from_datetime = from_datetime.replace(tzinfo=utc)
if not to_datetime.tzinfo:
to_datetime = to_datetime.replace(tzinfo=utc)
from_datetime = self._timezone_datetime(from_datetime)
to_datetime = self._timezone_datetime(to_datetime)
# total hours per day: retrieve attendances with one extra day margin,
# in order to compute the total hours on the first and last days
from_full = from_datetime - timedelta(days=1)
to_full = to_datetime + timedelta(days=1)
intervals = calendar._attendance_intervals(from_full, to_full, resource)
day_total = defaultdict(float)
for start, stop, meta in intervals:
day_total[start.date()] += (stop - start).total_seconds() / 3600
day_total = self._get_day_total(from_datetime, to_datetime, calendar, resource)
# actual hours per day
if compute_leaves:
intervals = calendar._work_intervals(from_datetime, to_datetime, resource, domain)
else:
intervals = calendar._attendance_intervals(from_datetime, to_datetime, resource)
day_hours = defaultdict(float)
for start, stop, meta in intervals:
day_hours[start.date()] += (stop - start).total_seconds() / 3600
# compute number of days as quarters
days = sum(
float_utils.round(ROUNDING_FACTOR * day_hours[day] / day_total[day]) / ROUNDING_FACTOR
for day in day_hours
)
return {
'days': days,
'hours': sum(day_hours.values()),
}
return self._get_days_data(intervals, day_total)
def get_leave_days_data(self, from_datetime, to_datetime, calendar=None, domain=None):
"""
@@ -116,36 +137,17 @@ class ResourceMixin(models.AbstractModel):
calendar = calendar or self.resource_calendar_id
# naive datetimes are made explicit in UTC
if not from_datetime.tzinfo:
from_datetime = from_datetime.replace(tzinfo=utc)
if not to_datetime.tzinfo:
to_datetime = to_datetime.replace(tzinfo=utc)
from_datetime = self._timezone_datetime(from_datetime)
to_datetime = self._timezone_datetime(to_datetime)
# total hours per day: retrieve attendances with one extra day margin,
# in order to compute the total hours on the first and last days
from_full = from_datetime - timedelta(days=1)
to_full = to_datetime + timedelta(days=1)
intervals = calendar._attendance_intervals(from_full, to_full, resource)
day_total = defaultdict(float)
for start, stop, meta in intervals:
day_total[start.date()] += (stop - start).total_seconds() / 3600
day_total = self._get_day_total(from_datetime, to_datetime, calendar, resource)
# compute actual hours per day
attendances = calendar._attendance_intervals(from_datetime, to_datetime, resource)
leaves = calendar._leave_intervals(from_datetime, to_datetime, resource, domain)
day_hours = defaultdict(float)
for start, stop, meta in (attendances & leaves):
day_hours[start.date()] += (stop - start).total_seconds() / 3600
# compute number of days as quarters
days = sum(
float_utils.round(ROUNDING_FACTOR * day_hours[day] / day_total[day]) / ROUNDING_FACTOR
for day in day_hours
)
return {
'days': days,
'hours': sum(day_hours.values()),
}
return self._get_days_data(attendances & leaves, day_total)
def list_work_time_per_day(self, from_datetime, to_datetime, calendar=None, domain=None):
"""