You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							669 lines
						
					
					
						
							33 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							669 lines
						
					
					
						
							33 KiB
						
					
					
				| # -*- 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") | |
|     # category_id = fields.Many2one(related='salary_rule_id.category_id', string='Category', required=True) | |
|     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_batch(from_full, to_full, resource) | |
|         day_total = defaultdict(float) | |
|         for start, stop, meta in intervals[resource.id]: | |
|             day_total[start.date()] += (stop - start).total_seconds() / 3600 | |
| 
 | |
|         # actual hours per day | |
|         if compute_leaves: | |
|             intervals = calendar._work_intervals_batch(from_datetime, to_datetime, resource, domain) | |
|         else: | |
|             intervals = calendar._attendance_intervals_batch(from_datetime, to_datetime, resource) | |
|         day_hours = defaultdict(float) | |
|         for start, stop, meta in intervals[resource.id]: | |
|             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()), | |
|         }
 | |
| 
 |