diff options
Diffstat (limited to 'hr_payroll_community/models/hr_payslip.py')
| -rw-r--r-- | hr_payroll_community/models/hr_payslip.py | 668 |
1 files changed, 668 insertions, 0 deletions
diff --git a/hr_payroll_community/models/hr_payslip.py b/hr_payroll_community/models/hr_payslip.py new file mode 100644 index 0000000..83b313e --- /dev/null +++ b/hr_payroll_community/models/hr_payslip.py @@ -0,0 +1,668 @@ +# -*- coding:utf-8 -*- + +import babel +from collections import defaultdict +from datetime import date, datetime, time +from datetime import timedelta +from dateutil.relativedelta import relativedelta +from pytz import timezone +from pytz import utc + +from odoo import api, fields, models, tools, _ +from odoo.addons import decimal_precision as dp +from odoo.exceptions import UserError, ValidationError +from odoo.tools import float_utils + +# This will generate 16th of days +ROUNDING_FACTOR = 16 + + +class HrPayslip(models.Model): + _name = 'hr.payslip' + _description = 'Pay Slip' + + struct_id = fields.Many2one('hr.payroll.structure', string='Structure', + readonly=True, states={'draft': [('readonly', False)]}, + help='Defines the rules that have to be applied to this payslip, accordingly ' + 'to the contract chosen. If you let empty the field contract, this field isn\'t ' + 'mandatory anymore and thus the rules applied will be all the rules set on the ' + 'structure of all contracts of the employee valid for the chosen period') + name = fields.Char(string='Payslip Name', readonly=True, + states={'draft': [('readonly', False)]}) + number = fields.Char(string='Reference', readonly=True, copy=False, help="References", + states={'draft': [('readonly', False)]}) + employee_id = fields.Many2one('hr.employee', string='Employee', required=True, readonly=True, help="Employee", + states={'draft': [('readonly', False)]}) + date_from = fields.Date(string='Date From', readonly=True, required=True, help="Start date", + default=lambda self: fields.Date.to_string(date.today().replace(day=1)), + states={'draft': [('readonly', False)]}) + date_to = fields.Date(string='Date To', readonly=True, required=True, help="End date", + default=lambda self: fields.Date.to_string( + (datetime.now() + relativedelta(months=+1, day=1, days=-1)).date()), + states={'draft': [('readonly', False)]}) + # this is chaos: 4 states are defined, 3 are used ('verify' isn't) and 5 exist ('confirm' seems to have existed) + state = fields.Selection([ + ('draft', 'Draft'), + ('verify', 'Waiting'), + ('done', 'Done'), + ('cancel', 'Rejected'), + ], string='Status', index=True, readonly=True, copy=False, default='draft', + help="""* When the payslip is created the status is \'Draft\' + \n* If the payslip is under verification, the status is \'Waiting\'. + \n* If the payslip is confirmed then status is set to \'Done\'. + \n* When user cancel payslip the status is \'Rejected\'.""") + line_ids = fields.One2many('hr.payslip.line', 'slip_id', string='Payslip Lines', readonly=True, + states={'draft': [('readonly', False)]}) + company_id = fields.Many2one('res.company', string='Company', readonly=True, copy=False, help="Company", + default=lambda self: self.env['res.company']._company_default_get(), + states={'draft': [('readonly', False)]}) + worked_days_line_ids = fields.One2many('hr.payslip.worked_days', 'payslip_id', + string='Payslip Worked Days', copy=True, readonly=True, + help="Payslip worked days", + states={'draft': [('readonly', False)]}) + input_line_ids = fields.One2many('hr.payslip.input', 'payslip_id', string='Payslip Inputs', + readonly=True, states={'draft': [('readonly', False)]}) + paid = fields.Boolean(string='Made Payment Order ? ', readonly=True, copy=False, + states={'draft': [('readonly', False)]}) + note = fields.Text(string='Internal Note', readonly=True, states={'draft': [('readonly', False)]}) + contract_id = fields.Many2one('hr.contract', string='Contract', readonly=True, help="Contract", + 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', help="Details from the 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") + payslip_run_id = fields.Many2one('hr.payslip.run', string='Payslip Batches', readonly=True, + copy=False, states={'draft': [('readonly', False)]}) + payslip_count = fields.Integer(compute='_compute_payslip_count', string="Payslip Computation Details") + + 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) + + def _compute_payslip_count(self): + for payslip in self: + payslip.payslip_count = len(payslip.line_ids) + + @api.constrains('date_from', 'date_to') + def _check_dates(self): + + if any(self.filtered(lambda payslip: payslip.date_from > payslip.date_to)): + raise ValidationError(_("Payslip 'Date From' must be earlier 'Date To'.")) + + def action_payslip_draft(self): + + return self.write({'state': 'draft'}) + + def action_payslip_done(self): + + self.compute_sheet() + return self.write({'state': 'done'}) + + def action_payslip_cancel(self): + + if self.filtered(lambda slip: slip.state == 'done'): + raise UserError(_("Cannot cancel a payslip that is done.")) + return self.write({'state': 'cancel'}) + + def refund_sheet(self): + for payslip in self: + + copied_payslip = payslip.copy({'credit_note': True, 'name': _('Refund: ') + payslip.name}) + copied_payslip.compute_sheet() + copied_payslip.action_payslip_done() + formview_ref = self.env.ref('hr_payroll_community.view_hr_payslip_form', False) + treeview_ref = self.env.ref('hr_payroll_community.view_hr_payslip_tree', False) + return { + 'name': ("Refund Payslip"), + 'view_mode': 'tree, form', + 'view_id': False, + 'res_model': 'hr.payslip', + 'type': 'ir.actions.act_window', + 'target': 'current', + 'domain': "[('id', 'in', %s)]" % copied_payslip.ids, + 'views': [(treeview_ref and treeview_ref.id or False, 'tree'), + (formview_ref and formview_ref.id or False, 'form')], + 'context': {} + } + + def check_done(self): + + return True + + def unlink(self): + + if any(self.filtered(lambda payslip: payslip.state not in ('draft', 'cancel'))): + raise UserError(_('You cannot delete a payslip which is not draft or cancelled!')) + return super(HrPayslip, self).unlink() + + # TODO move this function into hr_contract module, on hr.employee object + @api.model + def get_contract(self, employee, date_from, date_to): + + """ + @param employee: recordset of employee + @param date_from: date field + @param date_to: date field + @return: returns the ids of all the contracts for the given employee that need to be considered for the given dates + """ + # a contract is valid if it ends between the given dates + clause_1 = ['&', ('date_end', '<=', date_to), ('date_end', '>=', date_from)] + # OR if it starts between the given dates + clause_2 = ['&', ('date_start', '<=', date_to), ('date_start', '>=', date_from)] + # OR if it starts before the date_from and finish after the date_end (or never finish) + clause_3 = ['&', ('date_start', '<=', date_from), '|', ('date_end', '=', False), ('date_end', '>=', date_to)] + clause_final = [('employee_id', '=', employee.id), ('state', '=', 'open'), '|', + '|'] + clause_1 + clause_2 + clause_3 + return self.env['hr.contract'].search(clause_final).ids + + def compute_sheet(self): + + for payslip in self: + number = payslip.number or self.env['ir.sequence'].next_by_code('salary.slip') + # delete old payslip lines + payslip.line_ids.unlink() + # set the list of contract for which the rules have to be applied + # if we don't give the contract, then the rules to apply should be for all current contracts of the employee + contract_ids = payslip.contract_id.ids or \ + self.get_contract(payslip.employee_id, payslip.date_from, payslip.date_to) + lines = [(0, 0, line) for line in self._get_payslip_lines(contract_ids, payslip.id)] + payslip.write({'line_ids': lines, 'number': number}) + 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 + """ + res = [] + # fill only if the contract as a working schedule linked + for contract in contracts.filtered(lambda contract: contract.resource_calendar_id): + day_from = datetime.combine(fields.Date.from_string(date_from), time.min) + day_to = datetime.combine(fields.Date.from_string(date_to), time.max) + + # 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 or _('Global Leaves'), + 'sequence': 5, + 'code': holiday.holiday_status_id.code or 'GLOBAL', + '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, + } + + res.append(attendances) + res.extend(leaves.values()) + return res + + @api.model + def get_inputs(self, contracts, date_from, date_to): + + res = [] + + structure_ids = contracts.get_all_structures() + rule_ids = self.env['hr.payroll.structure'].browse(structure_ids).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 + + @api.model + def _get_payslip_lines(self, contract_ids, payslip_id): + + def _sum_salary_rule_category(localdict, category, amount): + if category.parent_id: + localdict = _sum_salary_rule_category(localdict, category.parent_id, amount) + localdict['categories'].dict[category.code] = category.code in localdict['categories'].dict and \ + localdict['categories'].dict[category.code] + amount or amount + return localdict + + class BrowsableObject(object): + def __init__(self, employee_id, dict, env): + self.employee_id = employee_id + self.dict = dict + self.env = env + + def __getattr__(self, attr): + return attr in self.dict and self.dict.__getitem__(attr) or 0.0 + + class InputLine(BrowsableObject): + """a class that will be used into the python code, mainly for usability purposes""" + + def sum(self, code, from_date, to_date=None): + if to_date is None: + to_date = fields.Date.today() + self.env.cr.execute(""" + SELECT sum(amount) as sum + FROM hr_payslip as hp, hr_payslip_input as pi + WHERE hp.employee_id = %s AND hp.state = 'done' + AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s""", + (self.employee_id, from_date, to_date, code)) + return self.env.cr.fetchone()[0] or 0.0 + + class WorkedDays(BrowsableObject): + """a class that will be used into the python code, mainly for usability purposes""" + + def _sum(self, code, from_date, to_date=None): + if to_date is None: + to_date = fields.Date.today() + self.env.cr.execute(""" + SELECT sum(number_of_days) as number_of_days, sum(number_of_hours) as number_of_hours + FROM hr_payslip as hp, hr_payslip_worked_days as pi + WHERE hp.employee_id = %s AND hp.state = 'done' + AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s""", + (self.employee_id, from_date, to_date, code)) + return self.env.cr.fetchone() + + def sum(self, code, from_date, to_date=None): + res = self._sum(code, from_date, to_date) + return res and res[0] or 0.0 + + def sum_hours(self, code, from_date, to_date=None): + res = self._sum(code, from_date, to_date) + return res and res[1] or 0.0 + + class Payslips(BrowsableObject): + """a class that will be used into the python code, mainly for usability purposes""" + + def sum(self, code, from_date, to_date=None): + if to_date is None: + to_date = fields.Date.today() + self.env.cr.execute("""SELECT sum(case when hp.credit_note = False then (pl.total) else (-pl.total) end) + FROM hr_payslip as hp, hr_payslip_line as pl + WHERE hp.employee_id = %s AND hp.state = 'done' + AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id AND pl.code = %s""", + (self.employee_id, from_date, to_date, code)) + res = self.env.cr.fetchone() + return res and res[0] or 0.0 + + # we keep a dict with the result because a value can be overwritten by another rule with the same code + result_dict = {} + rules_dict = {} + worked_days_dict = {} + inputs_dict = {} + blacklist = [] + payslip = self.env['hr.payslip'].browse(payslip_id) + for worked_days_line in payslip.worked_days_line_ids: + worked_days_dict[worked_days_line.code] = worked_days_line + for input_line in payslip.input_line_ids: + inputs_dict[input_line.code] = input_line + + categories = BrowsableObject(payslip.employee_id.id, {}, self.env) + inputs = InputLine(payslip.employee_id.id, inputs_dict, self.env) + worked_days = WorkedDays(payslip.employee_id.id, worked_days_dict, self.env) + payslips = Payslips(payslip.employee_id.id, payslip, self.env) + rules = BrowsableObject(payslip.employee_id.id, rules_dict, self.env) + + baselocaldict = {'categories': categories, 'rules': rules, 'payslip': payslips, 'worked_days': worked_days, + 'inputs': inputs} + # get the ids of the structures on the contracts and their parent id as well + contracts = self.env['hr.contract'].browse(contract_ids) + if len(contracts) == 1 and payslip.struct_id: + structure_ids = list(set(payslip.struct_id._get_parent_structure().ids)) + else: + structure_ids = contracts.get_all_structures() + # get the rules of the structure and thier children + rule_ids = self.env['hr.payroll.structure'].browse(structure_ids).get_all_rules() + # run the rules by sequence + sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x: x[1])] + sorted_rules = self.env['hr.salary.rule'].browse(sorted_rule_ids) + + for contract in contracts: + employee = contract.employee_id + localdict = dict(baselocaldict, employee=employee, contract=contract) + for rule in sorted_rules: + key = rule.code + '-' + str(contract.id) + localdict['result'] = None + localdict['result_qty'] = 1.0 + localdict['result_rate'] = 100 + # check if the rule can be applied + if rule._satisfy_condition(localdict) and rule.id not in blacklist: + # compute the amount of the rule + amount, qty, rate = rule._compute_rule(localdict) + # check if there is already a rule computed with that code + previous_amount = rule.code in localdict and localdict[rule.code] or 0.0 + # set/overwrite the amount computed for this rule in the localdict + tot_rule = amount * qty * rate / 100.0 + localdict[rule.code] = tot_rule + rules_dict[rule.code] = rule + # sum the amount for its salary category + localdict = _sum_salary_rule_category(localdict, rule.category_id, tot_rule - previous_amount) + # create/overwrite the rule in the temporary results + result_dict[key] = { + 'salary_rule_id': rule.id, + 'contract_id': contract.id, + 'name': rule.name, + 'code': rule.code, + 'category_id': rule.category_id.id, + 'sequence': rule.sequence, + 'appears_on_payslip': rule.appears_on_payslip, + 'condition_select': rule.condition_select, + 'condition_python': rule.condition_python, + 'condition_range': rule.condition_range, + 'condition_range_min': rule.condition_range_min, + 'condition_range_max': rule.condition_range_max, + 'amount_select': rule.amount_select, + 'amount_fix': rule.amount_fix, + 'amount_python_compute': rule.amount_python_compute, + 'amount_percentage': rule.amount_percentage, + 'amount_percentage_base': rule.amount_percentage_base, + 'register_id': rule.register_id.id, + 'amount': amount, + 'employee_id': contract.employee_id.id, + 'quantity': qty, + 'rate': rate, + } + else: + # blacklist this rule and its children + blacklist += [id for id, seq in rule._recursive_search_of_rules()] + + 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 + 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, + }) + + if not self.env.context.get('contract'): + # fill with the first contract of the employee + contract_ids = self.get_contract(employee, date_from, date_to) + else: + if contract_id: + # set the list of contract for which the input have to be filled + contract_ids = [contract_id] + else: + # if we don't give the contract, then the input to fill should be for all current contracts of the employee + contract_ids = self.get_contract(employee, date_from, date_to) + + if not contract_ids: + return res + contract = self.env['hr.contract'].browse(contract_ids[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 + contracts = self.env['hr.contract'].browse(contract_ids) + 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): + + + if (not self.employee_id) or (not self.date_from) or (not self.date_to): + return + + employee = self.employee_id + date_from = self.date_from + date_to = self.date_to + contract_ids = [] + + ttyme = datetime.combine(fields.Date.from_string(date_from), time.min) + locale = self.env.context.get('lang') or 'en_US' + self.name = _('Salary Slip of %s for %s') % ( + employee.name, tools.ustr(babel.dates.format_date(date=ttyme, format='MMMM-y', locale=locale))) + self.company_id = employee.company_id + + if not self.env.context.get('contract') or not self.contract_id: + contract_ids = self.get_contract(employee, date_from, date_to) + if not contract_ids: + return + self.contract_id = self.env['hr.contract'].browse(contract_ids[0]) + + if not self.contract_id.struct_id: + return + self.struct_id = self.contract_id.struct_id + if self.contract_id: + contract_ids = self.contract_id.ids + # computation of the salary input + contracts = self.env['hr.contract'].browse(contract_ids) + 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') + def onchange_contract(self): + + if not self.contract_id: + self.struct_id = False + self.with_context(contract=True).onchange_employee() + return + + def get_salary_line_total(self, code): + + self.ensure_one() + line = self.line_ids.filtered(lambda line: line.code == code) + if line: + return line[0].total + else: + return 0.0 + + +class HrPayslipLine(models.Model): + _name = 'hr.payslip.line' + _inherit = 'hr.salary.rule' + _description = 'Payslip Line' + _order = 'contract_id, sequence' + + slip_id = fields.Many2one('hr.payslip', string='Pay Slip', required=True, ondelete='cascade', help="Payslip") + salary_rule_id = fields.Many2one('hr.salary.rule', string='Rule', required=True, help="salary rule") + employee_id = fields.Many2one('hr.employee', string='Employee', required=True, help="Employee") + contract_id = fields.Many2one('hr.contract', string='Contract', required=True, index=True, help="Contract") + rate = fields.Float(string='Rate (%)', digits=dp.get_precision('Payroll Rate'), default=100.0) + amount = fields.Float(digits=dp.get_precision('Payroll')) + quantity = fields.Float(digits=dp.get_precision('Payroll'), default=1.0) + total = fields.Float(compute='_compute_total', string='Total', help="Total", digits=dp.get_precision('Payroll'), store=True) + + @api.depends('quantity', 'amount', 'rate') + def _compute_total(self): + + for line in self: + line.total = float(line.quantity) * line.amount * line.rate / 100 + + @api.model_create_multi + def create(self, vals_list): + + for values in vals_list: + if 'employee_id' not in values or 'contract_id' not in values: + payslip = self.env['hr.payslip'].browse(values.get('slip_id')) + values['employee_id'] = values.get('employee_id') or payslip.employee_id.id + values['contract_id'] = values.get('contract_id') or payslip.contract_id and payslip.contract_id.id + if not values['contract_id']: + raise UserError(_('You must set a contract to create a payslip line.')) + return super(HrPayslipLine, self).create(vals_list) + + +class HrPayslipWorkedDays(models.Model): + _name = 'hr.payslip.worked_days' + _description = 'Payslip Worked Days' + _order = 'payslip_id, sequence' + + name = fields.Char(string='Description', required=True) + payslip_id = fields.Many2one('hr.payslip', string='Pay Slip', required=True, ondelete='cascade', index=True, help="Payslip") + sequence = fields.Integer(required=True, index=True, default=10, help="Sequence") + 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', help="Number of days worked") + number_of_hours = fields.Float(string='Number of Hours', help="Number of hours worked") + contract_id = fields.Many2one('hr.contract', string='Contract', required=True, + help="The contract for which applied this input") + + +class HrPayslipInput(models.Model): + _name = 'hr.payslip.input' + _description = 'Payslip Input' + _order = 'payslip_id, sequence' + + name = fields.Char(string='Description', required=True) + payslip_id = fields.Many2one('hr.payslip', string='Pay Slip', required=True, ondelete='cascade', help="Payslip", index=True) + sequence = fields.Integer(required=True, index=True, default=10, help="Sequence") + code = fields.Char(required=True, help="The code that can be used in the salary rules") + 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, + help="The contract for which applied this input") + + +class HrPayslipRun(models.Model): + _name = 'hr.payslip.run' + _description = 'Payslip Batches' + + name = fields.Char(required=True, readonly=True, states={'draft': [('readonly', False)]}) + slip_ids = fields.One2many('hr.payslip', 'payslip_run_id', string='Payslips', readonly=True, + states={'draft': [('readonly', False)]}) + state = fields.Selection([ + ('draft', 'Draft'), + ('close', 'Close'), + ], string='Status', index=True, readonly=True, copy=False, default='draft') + date_start = fields.Date(string='Date From', required=True, readonly=True, help="start date", + states={'draft': [('readonly', False)]}, + default=lambda self: fields.Date.to_string(date.today().replace(day=1))) + date_end = fields.Date(string='Date To', required=True, readonly=True, help="End date", + states={'draft': [('readonly', False)]}, + default=lambda self: fields.Date.to_string( + (datetime.now() + relativedelta(months=+1, day=1, days=-1)).date())) + 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.") + + def draft_payslip_run(self): + return self.write({'state': 'draft'}) + + def close_payslip_run(self): + return self.write({'state': 'close'}) + + +class ResourceMixin(models.AbstractModel): + _inherit = "resource.mixin" + + 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 + changed using the `calendar` argument. + + `domain` is used in order to recognise the leaves to take, + None means default value ('time_type', '=', 'leave') + + Returns a dict {'days': n, 'hours': h} containing the + quantity of working time expressed as days and as hours. + """ + resource = self.resource_id + 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) + + # 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 + + # 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()), + } |
