From f857332bf85a3635e2a426ac056a579b86cceacd Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Mon, 14 May 2018 16:08:30 -0700 Subject: [PATCH] Initial commit of `hr_payroll_attendance` and `hr_payroll_attendance_holidays` modules for 11.0. The purpose of this new functionality is to better distinguish between timesheets and attendance, as well as preventing the stock "salaried" time computations from working. --- hr_payroll_attendance/__init__.py | 2 + hr_payroll_attendance/__manifest__.py | 16 ++++ hr_payroll_attendance/hr_contract.py | 7 ++ hr_payroll_attendance/hr_contract_view.xml | 22 +++++ hr_payroll_attendance/hr_payslip.py | 93 ++++++++++++++++++++++ 5 files changed, 140 insertions(+) create mode 100755 hr_payroll_attendance/__init__.py create mode 100755 hr_payroll_attendance/__manifest__.py create mode 100755 hr_payroll_attendance/hr_contract.py create mode 100755 hr_payroll_attendance/hr_contract_view.xml create mode 100755 hr_payroll_attendance/hr_payslip.py diff --git a/hr_payroll_attendance/__init__.py b/hr_payroll_attendance/__init__.py new file mode 100755 index 00000000..f1f0fe2e --- /dev/null +++ b/hr_payroll_attendance/__init__.py @@ -0,0 +1,2 @@ +from . import hr_payslip +from . import hr_contract diff --git a/hr_payroll_attendance/__manifest__.py b/hr_payroll_attendance/__manifest__.py new file mode 100755 index 00000000..4aa42648 --- /dev/null +++ b/hr_payroll_attendance/__manifest__.py @@ -0,0 +1,16 @@ +{ + 'name': 'Attendance on Payslips', + 'description': 'Get Attendence numbers onto Employee Payslips.', + 'version': '11.0.1.0.0', + 'website': 'https://hibou.io/', + 'author': 'Hibou Corp. ', + 'license': 'AGPL-3', + 'category': 'Human Resources', + 'data': [ + 'hr_contract_view.xml', + ], + 'depends': [ + 'hr_payroll', + 'hr_attendance', + ], +} diff --git a/hr_payroll_attendance/hr_contract.py b/hr_payroll_attendance/hr_contract.py new file mode 100755 index 00000000..ebb0381f --- /dev/null +++ b/hr_payroll_attendance/hr_contract.py @@ -0,0 +1,7 @@ +from odoo import models, fields + + +class HrContract(models.Model): + _inherit = 'hr.contract' + + paid_hourly_attendance = fields.Boolean(string="Paid Hourly Attendance", default=False) diff --git a/hr_payroll_attendance/hr_contract_view.xml b/hr_payroll_attendance/hr_contract_view.xml new file mode 100755 index 00000000..5022cc3c --- /dev/null +++ b/hr_payroll_attendance/hr_contract_view.xml @@ -0,0 +1,22 @@ + + + + + hr.contract.form.inherit + hr.contract + 20 + + + + + + + + / pay period + / hour + + + + + + diff --git a/hr_payroll_attendance/hr_payslip.py b/hr_payroll_attendance/hr_payslip.py new file mode 100755 index 00000000..e4561032 --- /dev/null +++ b/hr_payroll_attendance/hr_payslip.py @@ -0,0 +1,93 @@ +from datetime import datetime +from collections import defaultdict +from odoo import api, models +from odoo.exceptions import ValidationError +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT + + +class HrPayslip(models.Model): + _inherit = 'hr.payslip' + + @api.model + def get_worked_day_lines(self, contracts, date_from, date_to): + def create_empty_worked_lines(employee, contract, date_from, date_to): + attn = { + 'name': 'Attendance', + 'sequence': 10, + 'code': 'ATTN', + 'number_of_days': 0.0, + 'number_of_hours': 0.0, + 'contract_id': contract.id, + } + + valid_attn = [ + ('employee_id', '=', employee.id), + ('check_in', '>=', date_from), + ('check_in', '<=', date_to), + ] + return attn, valid_attn + + work = [] + for contract in contracts.filtered(lambda c: c.paid_hourly_attendance): + worked_attn, valid_attn = create_empty_worked_lines( + contract.employee_id, + contract, + date_from, + date_to + ) + days = set() + for attn in self.env['hr.attendance'].search(valid_attn): + if not attn.check_out: + raise ValidationError('This pay period must not have any open attendances.') + if attn.worked_hours: + # Avoid in/outs + attn_start_time = datetime.strptime(attn.check_in, DEFAULT_SERVER_DATETIME_FORMAT) + attn_iso = attn_start_time.isocalendar() + if not attn_iso in days: + worked_attn['number_of_days'] += 1 + days.add(attn_iso) + worked_attn['number_of_hours'] += attn.worked_hours + worked_attn['number_of_hours'] = round(worked_attn['number_of_hours'], 2) + work.append(worked_attn) + + res = super(HrPayslip, self).get_worked_day_lines(contracts.filtered(lambda c: not c.paid_hourly_attendance), date_from, date_to) + res.extend(work) + return res + + @api.multi + def hour_break_down(self, code): + """ + :param code: what kind of worked days you need aggregated + :return: dict: keys are isocalendar tuples, values are hours. + """ + self.ensure_one() + if code == 'ATTN': + attns = self.env['hr.attendance'].search([ + ('employee_id', '=', self.employee_id.id), + ('check_in', '>=', self.date_from), + ('check_in', '<=', self.date_to), + ]) + day_values = defaultdict(float) + for attn in attns: + if not attn.check_out: + raise ValidationError('This pay period must not have any open attendances.') + if attn.worked_hours: + # Avoid in/outs + attn_start_time = datetime.strptime(attn.check_in, DEFAULT_SERVER_DATETIME_FORMAT) + attn_iso = attn_start_time.isocalendar() + day_values[attn_iso] += attn.worked_hours + return day_values + elif hasattr(super(HrPayslip, self), 'hour_break_down'): + return super(HrPayslip, self).hour_break_down(code) + + @api.multi + def hours_break_down_week(self, code): + """ + :param code: hat kind of worked days you need aggregated + :return: dict: keys are isocalendar weeks, values are hours. + """ + days = self.hour_break_down(code) + weeks = defaultdict(float) + for isoday, hours in days.items(): + weeks[isoday[1]] += hours + return weeks