979 lines
49 KiB
Python
979 lines
49 KiB
Python
#-*- coding:utf-8 -*-
|
|
##############################################################################
|
|
#
|
|
# OpenERP, Open Source Management Solution
|
|
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
|
|
# d$
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
##############################################################################
|
|
|
|
import time
|
|
from datetime import date
|
|
from datetime import datetime
|
|
from datetime import timedelta
|
|
from dateutil import relativedelta
|
|
|
|
from openerp import api, tools
|
|
from openerp.osv import fields, osv
|
|
from openerp.tools.translate import _
|
|
import openerp.addons.decimal_precision as dp
|
|
|
|
from openerp.tools.safe_eval import safe_eval as eval
|
|
|
|
class hr_payroll_structure(osv.osv):
|
|
"""
|
|
Salary structure used to defined
|
|
- Basic
|
|
- Allowances
|
|
- Deductions
|
|
"""
|
|
|
|
_name = 'hr.payroll.structure'
|
|
_description = 'Salary Structure'
|
|
_columns = {
|
|
'name':fields.char('Name', required=True),
|
|
'code':fields.char('Reference', size=64, required=True),
|
|
'company_id':fields.many2one('res.company', 'Company', required=True, copy=False),
|
|
'note': fields.text('Description'),
|
|
'parent_id':fields.many2one('hr.payroll.structure', 'Parent'),
|
|
'children_ids':fields.one2many('hr.payroll.structure', 'parent_id', 'Children', copy=True),
|
|
'rule_ids':fields.many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id', 'Salary Rules'),
|
|
}
|
|
|
|
def _get_parent(self, cr, uid, context=None):
|
|
obj_model = self.pool.get('ir.model.data')
|
|
res = False
|
|
data_id = obj_model.search(cr, uid, [('model', '=', 'hr.payroll.structure'), ('name', '=', 'structure_base')])
|
|
if data_id:
|
|
res = obj_model.browse(cr, uid, data_id[0], context=context).res_id
|
|
return res
|
|
|
|
_defaults = {
|
|
'company_id': lambda self, cr, uid, context: \
|
|
self.pool.get('res.users').browse(cr, uid, uid,
|
|
context=context).company_id.id,
|
|
'parent_id': _get_parent,
|
|
}
|
|
|
|
_constraints = [
|
|
(osv.osv._check_recursion, 'Error ! You cannot create a recursive Salary Structure.', ['parent_id'])
|
|
]
|
|
|
|
def copy(self, cr, uid, id, default=None, context=None):
|
|
default = dict(default or {},
|
|
code=_("%s (copy)") % (self.browse(cr, uid, id, context=context).code))
|
|
return super(hr_payroll_structure, self).copy(cr, uid, id, default, context=context)
|
|
|
|
@api.cr_uid_ids_context
|
|
def get_all_rules(self, cr, uid, structure_ids, context=None):
|
|
"""
|
|
@param structure_ids: list of structure
|
|
@return: returns a list of tuple (id, sequence) of rules that are maybe to apply
|
|
"""
|
|
|
|
all_rules = []
|
|
for struct in self.browse(cr, uid, structure_ids, context=context):
|
|
all_rules += self.pool.get('hr.salary.rule')._recursive_search_of_rules(cr, uid, struct.rule_ids, context=context)
|
|
return all_rules
|
|
|
|
@api.cr_uid_ids_context
|
|
def _get_parent_structure(self, cr, uid, struct_ids, context=None):
|
|
if not struct_ids:
|
|
return []
|
|
parent = []
|
|
for struct in self.browse(cr, uid, struct_ids, context=context):
|
|
if struct.parent_id:
|
|
parent.append(struct.parent_id.id)
|
|
if parent:
|
|
parent = self._get_parent_structure(cr, uid, parent, context)
|
|
return parent + struct_ids
|
|
|
|
|
|
class hr_contract(osv.osv):
|
|
"""
|
|
Employee contract based on the visa, work permits
|
|
allows to configure different Salary structure
|
|
"""
|
|
|
|
_inherit = 'hr.contract'
|
|
_description = 'Employee Contract'
|
|
_columns = {
|
|
'struct_id': fields.many2one('hr.payroll.structure', 'Salary Structure'),
|
|
'schedule_pay': fields.selection([
|
|
('monthly', 'Monthly'),
|
|
('quarterly', 'Quarterly'),
|
|
('semi-annually', 'Semi-annually'),
|
|
('annually', 'Annually'),
|
|
('weekly', 'Weekly'),
|
|
('bi-weekly', 'Bi-weekly'),
|
|
('bi-monthly', 'Bi-monthly'),
|
|
], 'Scheduled Pay', select=True),
|
|
}
|
|
|
|
_defaults = {
|
|
'schedule_pay': 'monthly',
|
|
}
|
|
|
|
@api.cr_uid_ids_context
|
|
def get_all_structures(self, cr, uid, contract_ids, context=None):
|
|
"""
|
|
@param contract_ids: list of contracts
|
|
@return: the structures linked to the given contracts, ordered by hierachy (parent=False first, then first level children and so on) and without duplicata
|
|
"""
|
|
structure_ids = [contract.struct_id.id for contract in self.browse(cr, uid, contract_ids, context=context) if contract.struct_id]
|
|
if not structure_ids:
|
|
return []
|
|
return list(set(self.pool.get('hr.payroll.structure')._get_parent_structure(cr, uid, structure_ids, context=context)))
|
|
|
|
|
|
class contrib_register(osv.osv):
|
|
'''
|
|
Contribution Register
|
|
'''
|
|
|
|
_name = 'hr.contribution.register'
|
|
_description = 'Contribution Register'
|
|
|
|
_columns = {
|
|
'company_id':fields.many2one('res.company', 'Company'),
|
|
'partner_id':fields.many2one('res.partner', 'Partner'),
|
|
'name':fields.char('Name', required=True, readonly=False),
|
|
'register_line_ids':fields.one2many('hr.payslip.line', 'register_id', 'Register Line', readonly=True),
|
|
'note': fields.text('Description'),
|
|
}
|
|
_defaults = {
|
|
'company_id': lambda self, cr, uid, context: \
|
|
self.pool.get('res.users').browse(cr, uid, uid,
|
|
context=context).company_id.id,
|
|
}
|
|
|
|
|
|
class hr_salary_rule_category(osv.osv):
|
|
"""
|
|
HR Salary Rule Category
|
|
"""
|
|
|
|
_name = 'hr.salary.rule.category'
|
|
_description = 'Salary Rule Category'
|
|
_columns = {
|
|
'name':fields.char('Name', required=True, readonly=False),
|
|
'code':fields.char('Code', size=64, required=True, readonly=False),
|
|
'parent_id':fields.many2one('hr.salary.rule.category', 'Parent', help="Linking a salary category to its parent is used only for the reporting purpose."),
|
|
'children_ids': fields.one2many('hr.salary.rule.category', 'parent_id', 'Children'),
|
|
'note': fields.text('Description'),
|
|
'company_id':fields.many2one('res.company', 'Company', required=False),
|
|
}
|
|
|
|
_defaults = {
|
|
'company_id': lambda self, cr, uid, context: \
|
|
self.pool.get('res.users').browse(cr, uid, uid,
|
|
context=context).company_id.id,
|
|
}
|
|
|
|
|
|
class one2many_mod2(fields.one2many):
|
|
|
|
def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
|
|
if context is None:
|
|
context = {}
|
|
if not values:
|
|
values = {}
|
|
res = {}
|
|
for id in ids:
|
|
res[id] = []
|
|
ids2 = obj.pool[self._obj].search(cr, user, [(self._fields_id,'in',ids), ('appears_on_payslip', '=', True)], limit=self._limit)
|
|
for r in obj.pool[self._obj].read(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
|
|
key = r[self._fields_id]
|
|
if isinstance(key, tuple):
|
|
# Read return a tuple in the case where the field is a many2one
|
|
# but we want to get the id of this field.
|
|
key = key[0]
|
|
|
|
res[key].append( r['id'] )
|
|
return res
|
|
|
|
class hr_payslip_run(osv.osv):
|
|
|
|
_name = 'hr.payslip.run'
|
|
_description = 'Payslip Batches'
|
|
_columns = {
|
|
'name': fields.char('Name', required=True, readonly=True, states={'draft': [('readonly', False)]}),
|
|
'slip_ids': fields.one2many('hr.payslip', 'payslip_run_id', 'Payslips', required=False, readonly=True, states={'draft': [('readonly', False)]}),
|
|
'state': fields.selection([
|
|
('draft', 'Draft'),
|
|
('close', 'Close'),
|
|
], 'Status', select=True, readonly=True, copy=False),
|
|
'date_start': fields.date('Date From', required=True, readonly=True, states={'draft': [('readonly', False)]}),
|
|
'date_end': fields.date('Date To', required=True, readonly=True, states={'draft': [('readonly', False)]}),
|
|
'credit_note': fields.boolean('Credit Note', readonly=True, states={'draft': [('readonly', False)]}, help="If its checked, indicates that all payslips generated from here are refund payslips."),
|
|
}
|
|
_defaults = {
|
|
'state': 'draft',
|
|
'date_start': lambda *a: time.strftime('%Y-%m-01'),
|
|
'date_end': lambda *a: str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10],
|
|
}
|
|
|
|
def draft_payslip_run(self, cr, uid, ids, context=None):
|
|
return self.write(cr, uid, ids, {'state': 'draft'}, context=context)
|
|
|
|
def close_payslip_run(self, cr, uid, ids, context=None):
|
|
return self.write(cr, uid, ids, {'state': 'close'}, context=context)
|
|
|
|
|
|
class hr_payslip(osv.osv):
|
|
'''
|
|
Pay Slip
|
|
'''
|
|
|
|
_name = 'hr.payslip'
|
|
_description = 'Pay Slip'
|
|
|
|
def _get_lines_salary_rule_category(self, cr, uid, ids, field_names, arg=None, context=None):
|
|
result = {}
|
|
if not ids: return result
|
|
for id in ids:
|
|
result.setdefault(id, [])
|
|
cr.execute('''SELECT pl.slip_id, pl.id FROM hr_payslip_line AS pl \
|
|
LEFT JOIN hr_salary_rule_category AS sh on (pl.category_id = sh.id) \
|
|
WHERE pl.slip_id in %s \
|
|
GROUP BY pl.slip_id, pl.sequence, pl.id ORDER BY pl.sequence''',(tuple(ids),))
|
|
res = cr.fetchall()
|
|
for r in res:
|
|
result[r[0]].append(r[1])
|
|
return result
|
|
|
|
def _count_detail_payslip(self, cr, uid, ids, field_name, arg, context=None):
|
|
res = {}
|
|
for details in self.browse(cr, uid, ids, context=context):
|
|
res[details.id] = len(details.line_ids)
|
|
return res
|
|
|
|
_columns = {
|
|
'struct_id': fields.many2one('hr.payroll.structure', '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('Payslip Name', required=False, readonly=True, states={'draft': [('readonly', False)]}),
|
|
'number': fields.char('Reference', required=False, readonly=True, states={'draft': [('readonly', False)]}, copy=False),
|
|
'employee_id': fields.many2one('hr.employee', 'Employee', required=True, readonly=True, states={'draft': [('readonly', False)]}),
|
|
'date_from': fields.date('Date From', readonly=True, states={'draft': [('readonly', False)]}, required=True),
|
|
'date_to': fields.date('Date To', readonly=True, states={'draft': [('readonly', False)]}, required=True),
|
|
'state': fields.selection([
|
|
('draft', 'Draft'),
|
|
('verify', 'Waiting'),
|
|
('done', 'Done'),
|
|
('cancel', 'Rejected'),
|
|
], 'Status', select=True, readonly=True, copy=False,
|
|
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': one2many_mod2('hr.payslip.line', 'slip_id', 'Payslip Lines', readonly=True, states={'draft':[('readonly',False)]}),
|
|
'company_id': fields.many2one('res.company', 'Company', required=False, readonly=True, states={'draft': [('readonly', False)]}, copy=False),
|
|
'worked_days_line_ids': fields.one2many('hr.payslip.worked_days', 'payslip_id', 'Payslip Worked Days', copy=True, required=False, readonly=True, states={'draft': [('readonly', False)]}),
|
|
'input_line_ids': fields.one2many('hr.payslip.input', 'payslip_id', 'Payslip Inputs', required=False, readonly=True, states={'draft': [('readonly', False)]}),
|
|
'paid': fields.boolean('Made Payment Order ? ', required=False, readonly=True, states={'draft': [('readonly', False)]}, copy=False),
|
|
'note': fields.text('Internal Note', readonly=True, states={'draft':[('readonly',False)]}),
|
|
'contract_id': fields.many2one('hr.contract', 'Contract', required=False, readonly=True, states={'draft': [('readonly', False)]}),
|
|
'details_by_salary_rule_category': fields.function(_get_lines_salary_rule_category, method=True, type='one2many', relation='hr.payslip.line', string='Details by Salary Rule Category'),
|
|
'credit_note': fields.boolean('Credit Note', help="Indicates this payslip has a refund of another", readonly=True, states={'draft': [('readonly', False)]}),
|
|
'payslip_run_id': fields.many2one('hr.payslip.run', 'Payslip Batches', readonly=True, states={'draft': [('readonly', False)]}, copy=False),
|
|
'payslip_count': fields.function(_count_detail_payslip, type='integer', string="Payslip Computation Details"),
|
|
}
|
|
_defaults = {
|
|
'date_from': lambda *a: time.strftime('%Y-%m-01'),
|
|
'date_to': lambda *a: str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10],
|
|
'state': 'draft',
|
|
'credit_note': False,
|
|
'company_id': lambda self, cr, uid, context: \
|
|
self.pool.get('res.users').browse(cr, uid, uid,
|
|
context=context).company_id.id,
|
|
}
|
|
|
|
def _check_dates(self, cr, uid, ids, context=None):
|
|
for payslip in self.browse(cr, uid, ids, context=context):
|
|
if payslip.date_from > payslip.date_to:
|
|
return False
|
|
return True
|
|
|
|
_constraints = [(_check_dates, "Payslip 'Date From' must be before 'Date To'.", ['date_from', 'date_to'])]
|
|
|
|
def cancel_sheet(self, cr, uid, ids, context=None):
|
|
return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
|
|
|
|
def process_sheet(self, cr, uid, ids, context=None):
|
|
return self.write(cr, uid, ids, {'paid': True, 'state': 'done'}, context=context)
|
|
|
|
def hr_verify_sheet(self, cr, uid, ids, context=None):
|
|
self.compute_sheet(cr, uid, ids, context)
|
|
return self.write(cr, uid, ids, {'state': 'verify'}, context=context)
|
|
|
|
def refund_sheet(self, cr, uid, ids, context=None):
|
|
mod_obj = self.pool.get('ir.model.data')
|
|
for payslip in self.browse(cr, uid, ids, context=context):
|
|
id_copy = self.copy(cr, uid, payslip.id, {'credit_note': True, 'name': _('Refund: ')+payslip.name}, context=context)
|
|
self.signal_workflow(cr, uid, [id_copy], 'hr_verify_sheet')
|
|
self.signal_workflow(cr, uid, [id_copy], 'process_sheet')
|
|
|
|
form_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_form')
|
|
form_res = form_id and form_id[1] or False
|
|
tree_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_tree')
|
|
tree_res = tree_id and tree_id[1] or False
|
|
return {
|
|
'name':_("Refund Payslip"),
|
|
'view_mode': 'tree, form',
|
|
'view_id': False,
|
|
'view_type': 'form',
|
|
'res_model': 'hr.payslip',
|
|
'type': 'ir.actions.act_window',
|
|
'nodestroy': True,
|
|
'target': 'current',
|
|
'domain': "[('id', 'in', %s)]" % [id_copy],
|
|
'views': [(tree_res, 'tree'), (form_res, 'form')],
|
|
'context': {}
|
|
}
|
|
|
|
def check_done(self, cr, uid, ids, context=None):
|
|
return True
|
|
|
|
def unlink(self, cr, uid, ids, context=None):
|
|
for payslip in self.browse(cr, uid, ids, context=context):
|
|
if payslip.state not in ['draft','cancel']:
|
|
raise osv.except_osv(_('Warning!'),_('You cannot delete a payslip which is not draft or cancelled!'))
|
|
return super(hr_payslip, self).unlink(cr, uid, ids, context)
|
|
|
|
#TODO move this function into hr_contract module, on hr.employee object
|
|
def get_contract(self, cr, uid, employee, date_from, date_to, context=None):
|
|
"""
|
|
@param employee: browse record 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
|
|
"""
|
|
contract_obj = self.pool.get('hr.contract')
|
|
clause = []
|
|
#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),'|','|'] + clause_1 + clause_2 + clause_3
|
|
contract_ids = contract_obj.search(cr, uid, clause_final, context=context)
|
|
return contract_ids
|
|
|
|
def compute_sheet(self, cr, uid, ids, context=None):
|
|
slip_line_pool = self.pool.get('hr.payslip.line')
|
|
sequence_obj = self.pool.get('ir.sequence')
|
|
for payslip in self.browse(cr, uid, ids, context=context):
|
|
number = payslip.number or sequence_obj.get(cr, uid, 'salary.slip')
|
|
#delete old payslip lines
|
|
old_slipline_ids = slip_line_pool.search(cr, uid, [('slip_id', '=', payslip.id)], context=context)
|
|
# old_slipline_ids
|
|
if old_slipline_ids:
|
|
slip_line_pool.unlink(cr, uid, old_slipline_ids, context=context)
|
|
if payslip.contract_id:
|
|
#set the list of contract for which the rules have to be applied
|
|
contract_ids = [payslip.contract_id.id]
|
|
else:
|
|
#if we don't give the contract, then the rules to apply should be for all current contracts of the employee
|
|
contract_ids = self.get_contract(cr, uid, payslip.employee_id, payslip.date_from, payslip.date_to, context=context)
|
|
lines = [(0,0,line) for line in self.pool.get('hr.payslip').get_payslip_lines(cr, uid, contract_ids, payslip.id, context=context)]
|
|
self.write(cr, uid, [payslip.id], {'line_ids': lines, 'number': number,}, context=context)
|
|
return True
|
|
|
|
def get_worked_day_lines(self, cr, uid, contract_ids, date_from, date_to, context=None):
|
|
"""
|
|
@param contract_ids: list of contract id
|
|
@return: returns a list of dict containing the input that should be applied for the given contract between date_from and date_to
|
|
"""
|
|
def was_on_leave(employee_id, datetime_day, context=None):
|
|
res = False
|
|
day = datetime_day.strftime("%Y-%m-%d")
|
|
holiday_ids = self.pool.get('hr.holidays').search(cr, uid, [('state','=','validate'),('employee_id','=',employee_id),('type','=','remove'),('date_from','<=',day),('date_to','>=',day)])
|
|
if holiday_ids:
|
|
res = self.pool.get('hr.holidays').browse(cr, uid, holiday_ids, context=context)[0].holiday_status_id.name
|
|
return res
|
|
|
|
res = []
|
|
for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
|
|
if not contract.working_hours:
|
|
#fill only if the contract as a working schedule linked
|
|
continue
|
|
attendances = {
|
|
'name': _("Normal Working Days paid at 100%"),
|
|
'sequence': 1,
|
|
'code': 'WORK100',
|
|
'number_of_days': 0.0,
|
|
'number_of_hours': 0.0,
|
|
'contract_id': contract.id,
|
|
}
|
|
leaves = {}
|
|
day_from = datetime.strptime(date_from,"%Y-%m-%d")
|
|
day_to = datetime.strptime(date_to,"%Y-%m-%d")
|
|
nb_of_days = (day_to - day_from).days + 1
|
|
for day in range(0, nb_of_days):
|
|
working_hours_on_day = self.pool.get('resource.calendar').working_hours_on_day(cr, uid, contract.working_hours, day_from + timedelta(days=day), context)
|
|
if working_hours_on_day:
|
|
#the employee had to work
|
|
leave_type = was_on_leave(contract.employee_id.id, day_from + timedelta(days=day), context=context)
|
|
if leave_type:
|
|
#if he was on leave, fill the leaves dict
|
|
if leave_type in leaves:
|
|
leaves[leave_type]['number_of_days'] += 1.0
|
|
leaves[leave_type]['number_of_hours'] += working_hours_on_day
|
|
else:
|
|
leaves[leave_type] = {
|
|
'name': leave_type,
|
|
'sequence': 5,
|
|
'code': leave_type,
|
|
'number_of_days': 1.0,
|
|
'number_of_hours': working_hours_on_day,
|
|
'contract_id': contract.id,
|
|
}
|
|
else:
|
|
#add the input vals to tmp (increment if existing)
|
|
attendances['number_of_days'] += 1.0
|
|
attendances['number_of_hours'] += working_hours_on_day
|
|
leaves = [value for key,value in leaves.items()]
|
|
res += [attendances] + leaves
|
|
return res
|
|
|
|
def get_inputs(self, cr, uid, contract_ids, date_from, date_to, context=None):
|
|
res = []
|
|
contract_obj = self.pool.get('hr.contract')
|
|
rule_obj = self.pool.get('hr.salary.rule')
|
|
|
|
structure_ids = contract_obj.get_all_structures(cr, uid, contract_ids, context=context)
|
|
rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
|
|
sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
|
|
|
|
for contract in contract_obj.browse(cr, uid, contract_ids, context=context):
|
|
for rule in rule_obj.browse(cr, uid, sorted_rule_ids, context=context):
|
|
if rule.input_ids:
|
|
for input in rule.input_ids:
|
|
inputs = {
|
|
'name': input.name,
|
|
'code': input.code,
|
|
'contract_id': contract.id,
|
|
}
|
|
res += [inputs]
|
|
return res
|
|
|
|
def get_payslip_lines(self, cr, uid, contract_ids, payslip_id, context):
|
|
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, pool, cr, uid, employee_id, dict):
|
|
self.pool = pool
|
|
self.cr = cr
|
|
self.uid = uid
|
|
self.employee_id = employee_id
|
|
self.dict = dict
|
|
|
|
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 = datetime.now().strftime('%Y-%m-%d')
|
|
result = 0.0
|
|
self.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))
|
|
res = self.cr.fetchone()[0]
|
|
return res 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 = datetime.now().strftime('%Y-%m-%d')
|
|
result = 0.0
|
|
self.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.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 = datetime.now().strftime('%Y-%m-%d')
|
|
self.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.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 = {}
|
|
categories_dict = {}
|
|
blacklist = []
|
|
payslip_obj = self.pool.get('hr.payslip')
|
|
inputs_obj = self.pool.get('hr.payslip.worked_days')
|
|
obj_rule = self.pool.get('hr.salary.rule')
|
|
payslip = payslip_obj.browse(cr, uid, payslip_id, context=context)
|
|
worked_days = {}
|
|
for worked_days_line in payslip.worked_days_line_ids:
|
|
worked_days[worked_days_line.code] = worked_days_line
|
|
inputs = {}
|
|
for input_line in payslip.input_line_ids:
|
|
inputs[input_line.code] = input_line
|
|
|
|
categories_obj = BrowsableObject(self.pool, cr, uid, payslip.employee_id.id, categories_dict)
|
|
input_obj = InputLine(self.pool, cr, uid, payslip.employee_id.id, inputs)
|
|
worked_days_obj = WorkedDays(self.pool, cr, uid, payslip.employee_id.id, worked_days)
|
|
payslip_obj = Payslips(self.pool, cr, uid, payslip.employee_id.id, payslip)
|
|
rules_obj = BrowsableObject(self.pool, cr, uid, payslip.employee_id.id, rules)
|
|
|
|
baselocaldict = {'categories': categories_obj, 'rules': rules_obj, 'payslip': payslip_obj, 'worked_days': worked_days_obj, 'inputs': input_obj}
|
|
#get the ids of the structures on the contracts and their parent id as well
|
|
structure_ids = self.pool.get('hr.contract').get_all_structures(cr, uid, contract_ids, context=context)
|
|
#get the rules of the structure and thier children
|
|
rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
|
|
#run the rules by sequence
|
|
sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
|
|
|
|
for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
|
|
employee = contract.employee_id
|
|
localdict = dict(baselocaldict, employee=employee, contract=contract)
|
|
for rule in obj_rule.browse(cr, uid, sorted_rule_ids, context=context):
|
|
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 obj_rule.satisfy_condition(cr, uid, rule.id, localdict, context=context) and rule.id not in blacklist:
|
|
#compute the amount of the rule
|
|
amount, qty, rate = obj_rule.compute_rule(cr, uid, rule.id, localdict, context=context)
|
|
#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[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 self.pool.get('hr.salary.rule')._recursive_search_of_rules(cr, uid, [rule], context=context)]
|
|
|
|
result = [value for code, value in result_dict.items()]
|
|
return result
|
|
|
|
def onchange_employee_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
|
|
empolyee_obj = self.pool.get('hr.employee')
|
|
contract_obj = self.pool.get('hr.contract')
|
|
worked_days_obj = self.pool.get('hr.payslip.worked_days')
|
|
input_obj = self.pool.get('hr.payslip.input')
|
|
|
|
if context is None:
|
|
context = {}
|
|
#delete old worked days lines
|
|
old_worked_days_ids = ids and worked_days_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
|
|
if old_worked_days_ids:
|
|
worked_days_obj.unlink(cr, uid, old_worked_days_ids, context=context)
|
|
|
|
#delete old input lines
|
|
old_input_ids = ids and input_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
|
|
if old_input_ids:
|
|
input_obj.unlink(cr, uid, old_input_ids, context=context)
|
|
|
|
|
|
#defaults
|
|
res = {'value':{
|
|
'line_ids':[],
|
|
'input_line_ids': [],
|
|
'worked_days_line_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.fromtimestamp(time.mktime(time.strptime(date_from, "%Y-%m-%d")))
|
|
employee_id = empolyee_obj.browse(cr, uid, employee_id, context=context)
|
|
res['value'].update({
|
|
'name': _('Salary Slip of %s for %s') % (employee_id.name, tools.ustr(ttyme.strftime('%B-%Y'))),
|
|
'company_id': employee_id.company_id.id
|
|
})
|
|
|
|
if not context.get('contract', False):
|
|
#fill with the first contract of the employee
|
|
contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
|
|
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(cr, uid, employee_id, date_from, date_to, context=context)
|
|
|
|
if not contract_ids:
|
|
return res
|
|
contract_record = contract_obj.browse(cr, uid, contract_ids[0], context=context)
|
|
res['value'].update({
|
|
'contract_id': contract_record and contract_record.id or False
|
|
})
|
|
struct_record = contract_record and contract_record.struct_id or False
|
|
if not struct_record:
|
|
return res
|
|
res['value'].update({
|
|
'struct_id': struct_record.id,
|
|
})
|
|
#computation of the salary input
|
|
worked_days_line_ids = self.get_worked_day_lines(cr, uid, contract_ids, date_from, date_to, context=context)
|
|
input_line_ids = self.get_inputs(cr, uid, contract_ids, date_from, date_to, context=context)
|
|
res['value'].update({
|
|
'worked_days_line_ids': worked_days_line_ids,
|
|
'input_line_ids': input_line_ids,
|
|
})
|
|
return res
|
|
|
|
def onchange_contract_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
|
|
#TODO it seems to be the mess in the onchanges, we should have onchange_employee => onchange_contract => doing all the things
|
|
res = {'value':{
|
|
'line_ids': [],
|
|
'name': '',
|
|
}
|
|
}
|
|
context = dict(context or {}, contract=True)
|
|
if not contract_id:
|
|
res['value'].update({'struct_id': False})
|
|
return self.onchange_employee_id(cr, uid, ids, date_from=date_from, date_to=date_to, employee_id=employee_id, contract_id=contract_id, context=context)
|
|
|
|
|
|
class hr_payslip_worked_days(osv.osv):
|
|
'''
|
|
Payslip Worked Days
|
|
'''
|
|
|
|
_name = 'hr.payslip.worked_days'
|
|
_description = 'Payslip Worked Days'
|
|
_columns = {
|
|
'name': fields.char('Description', required=True),
|
|
'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade', select=True),
|
|
'sequence': fields.integer('Sequence', required=True, select=True),
|
|
'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
|
|
'number_of_days': fields.float('Number of Days'),
|
|
'number_of_hours': fields.float('Number of Hours'),
|
|
'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
|
|
}
|
|
_order = 'payslip_id, sequence'
|
|
_defaults = {
|
|
'sequence': 10,
|
|
}
|
|
|
|
class hr_payslip_input(osv.osv):
|
|
'''
|
|
Payslip Input
|
|
'''
|
|
|
|
_name = 'hr.payslip.input'
|
|
_description = 'Payslip Input'
|
|
_columns = {
|
|
'name': fields.char('Description', required=True),
|
|
'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade', select=True),
|
|
'sequence': fields.integer('Sequence', required=True, select=True),
|
|
'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
|
|
'amount': fields.float('Amount', 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', 'Contract', required=True, help="The contract for which applied this input"),
|
|
}
|
|
_order = 'payslip_id, sequence'
|
|
_defaults = {
|
|
'sequence': 10,
|
|
'amount': 0.0,
|
|
}
|
|
|
|
|
|
class hr_salary_rule(osv.osv):
|
|
|
|
_name = 'hr.salary.rule'
|
|
_columns = {
|
|
'name':fields.char('Name', required=True, readonly=False),
|
|
'code':fields.char('Code', size=64, required=True, help="The code of salary rules can be used as reference in computation of other rules. In that case, it is case sensitive."),
|
|
'sequence': fields.integer('Sequence', required=True, help='Use to arrange calculation sequence', select=True),
|
|
'quantity': fields.char('Quantity', help="It is used in computation for percentage and fixed amount.For e.g. A rule for Meal Voucher having fixed amount of 1€ per worked day can have its quantity defined in expression like worked_days.WORK100.number_of_days."),
|
|
'category_id':fields.many2one('hr.salary.rule.category', 'Category', required=True),
|
|
'active':fields.boolean('Active', help="If the active field is set to false, it will allow you to hide the salary rule without removing it."),
|
|
'appears_on_payslip': fields.boolean('Appears on Payslip', help="Used to display the salary rule on payslip."),
|
|
'parent_rule_id':fields.many2one('hr.salary.rule', 'Parent Salary Rule', select=True),
|
|
'company_id':fields.many2one('res.company', 'Company', required=False),
|
|
'condition_select': fields.selection([('none', 'Always True'),('range', 'Range'), ('python', 'Python Expression')], "Condition Based on", required=True),
|
|
'condition_range':fields.char('Range Based on', readonly=False, help='This will be used to compute the % fields values; in general it is on basic, but you can also use categories code fields in lowercase as a variable names (hra, ma, lta, etc.) and the variable basic.'),
|
|
'condition_python':fields.text('Python Condition', required=True, readonly=False, help='Applied this rule for calculation if condition is true. You can specify condition like basic > 1000.'),
|
|
'condition_range_min': fields.float('Minimum Range', required=False, help="The minimum amount, applied for this rule."),
|
|
'condition_range_max': fields.float('Maximum Range', required=False, help="The maximum amount, applied for this rule."),
|
|
'amount_select':fields.selection([
|
|
('percentage','Percentage (%)'),
|
|
('fix','Fixed Amount'),
|
|
('code','Python Code'),
|
|
],'Amount Type', select=True, required=True, help="The computation method for the rule amount."),
|
|
'amount_fix': fields.float('Fixed Amount', digits_compute=dp.get_precision('Payroll'),),
|
|
'amount_percentage': fields.float('Percentage (%)', digits_compute=dp.get_precision('Payroll Rate'), help='For example, enter 50.0 to apply a percentage of 50%'),
|
|
'amount_python_compute':fields.text('Python Code'),
|
|
'amount_percentage_base': fields.char('Percentage based on', required=False, readonly=False, help='result will be affected to a variable'),
|
|
'child_ids':fields.one2many('hr.salary.rule', 'parent_rule_id', 'Child Salary Rule', copy=True),
|
|
'register_id':fields.many2one('hr.contribution.register', 'Contribution Register', help="Eventual third party involved in the salary payment of the employees."),
|
|
'input_ids': fields.one2many('hr.rule.input', 'input_id', 'Inputs', copy=True),
|
|
'note':fields.text('Description'),
|
|
}
|
|
_defaults = {
|
|
'amount_python_compute': '''
|
|
# Available variables:
|
|
#----------------------
|
|
# payslip: object containing the payslips
|
|
# employee: hr.employee object
|
|
# contract: hr.contract object
|
|
# 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.
|
|
|
|
# Note: returned value have to be set in the variable 'result'
|
|
|
|
result = contract.wage * 0.10''',
|
|
'condition_python':
|
|
'''
|
|
# Available variables:
|
|
#----------------------
|
|
# payslip: object containing the payslips
|
|
# employee: hr.employee object
|
|
# contract: hr.contract object
|
|
# 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
|
|
|
|
# Note: returned value have to be set in the variable 'result'
|
|
|
|
result = rules.NET > categories.NET * 0.10''',
|
|
'condition_range': 'contract.wage',
|
|
'sequence': 5,
|
|
'appears_on_payslip': True,
|
|
'active': True,
|
|
'company_id': lambda self, cr, uid, context: \
|
|
self.pool.get('res.users').browse(cr, uid, uid,
|
|
context=context).company_id.id,
|
|
'condition_select': 'none',
|
|
'amount_select': 'fix',
|
|
'amount_fix': 0.0,
|
|
'amount_percentage': 0.0,
|
|
'quantity': '1.0',
|
|
}
|
|
|
|
@api.cr_uid_ids_context
|
|
def _recursive_search_of_rules(self, cr, uid, rule_ids, context=None):
|
|
"""
|
|
@param rule_ids: list of browse record
|
|
@return: returns a list of tuple (id, sequence) which are all the children of the passed rule_ids
|
|
"""
|
|
children_rules = []
|
|
for rule in rule_ids:
|
|
if rule.child_ids:
|
|
children_rules += self._recursive_search_of_rules(cr, uid, rule.child_ids, context=context)
|
|
return [(r.id, r.sequence) for r in rule_ids] + children_rules
|
|
|
|
#TODO should add some checks on the type of result (should be float)
|
|
def compute_rule(self, cr, uid, rule_id, localdict, context=None):
|
|
"""
|
|
:param rule_id: id of rule to compute
|
|
:param localdict: dictionary containing the environement in which to compute the rule
|
|
:return: returns a tuple build as the base/amount computed, the quantity and the rate
|
|
:rtype: (float, float, float)
|
|
"""
|
|
rule = self.browse(cr, uid, rule_id, context=context)
|
|
if rule.amount_select == 'fix':
|
|
try:
|
|
return rule.amount_fix, float(eval(rule.quantity, localdict)), 100.0
|
|
except:
|
|
raise osv.except_osv(_('Error!'), _('Wrong quantity defined for salary rule %s (%s).')% (rule.name, rule.code))
|
|
elif rule.amount_select == 'percentage':
|
|
try:
|
|
return (float(eval(rule.amount_percentage_base, localdict)),
|
|
float(eval(rule.quantity, localdict)),
|
|
rule.amount_percentage)
|
|
except:
|
|
raise osv.except_osv(_('Error!'), _('Wrong percentage base or quantity defined for salary rule %s (%s).')% (rule.name, rule.code))
|
|
else:
|
|
try:
|
|
eval(rule.amount_python_compute, localdict, mode='exec', nocopy=True)
|
|
return float(localdict['result']), 'result_qty' in localdict and localdict['result_qty'] or 1.0, 'result_rate' in localdict and localdict['result_rate'] or 100.0
|
|
except:
|
|
raise osv.except_osv(_('Error!'), _('Wrong python code defined for salary rule %s (%s).')% (rule.name, rule.code))
|
|
|
|
def satisfy_condition(self, cr, uid, rule_id, localdict, context=None):
|
|
"""
|
|
@param rule_id: id of hr.salary.rule to be tested
|
|
@param contract_id: id of hr.contract to be tested
|
|
@return: returns True if the given rule match the condition for the given contract. Return False otherwise.
|
|
"""
|
|
rule = self.browse(cr, uid, rule_id, context=context)
|
|
|
|
if rule.condition_select == 'none':
|
|
return True
|
|
elif rule.condition_select == 'range':
|
|
try:
|
|
result = eval(rule.condition_range, localdict)
|
|
return rule.condition_range_min <= result and result <= rule.condition_range_max or False
|
|
except:
|
|
raise osv.except_osv(_('Error!'), _('Wrong range condition defined for salary rule %s (%s).')% (rule.name, rule.code))
|
|
else: #python code
|
|
try:
|
|
eval(rule.condition_python, localdict, mode='exec', nocopy=True)
|
|
return 'result' in localdict and localdict['result'] or False
|
|
except:
|
|
raise osv.except_osv(_('Error!'), _('Wrong python condition defined for salary rule %s (%s).')% (rule.name, rule.code))
|
|
|
|
|
|
class hr_rule_input(osv.osv):
|
|
'''
|
|
Salary Rule Input
|
|
'''
|
|
|
|
_name = 'hr.rule.input'
|
|
_description = 'Salary Rule Input'
|
|
_columns = {
|
|
'name': fields.char('Description', required=True),
|
|
'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
|
|
'input_id': fields.many2one('hr.salary.rule', 'Salary Rule Input', required=True)
|
|
}
|
|
|
|
|
|
class hr_payslip_line(osv.osv):
|
|
'''
|
|
Payslip Line
|
|
'''
|
|
|
|
_name = 'hr.payslip.line'
|
|
_inherit = 'hr.salary.rule'
|
|
_description = 'Payslip Line'
|
|
_order = 'contract_id, sequence'
|
|
|
|
def _calculate_total(self, cr, uid, ids, name, args, context):
|
|
if not ids: return {}
|
|
res = {}
|
|
for line in self.browse(cr, uid, ids, context=context):
|
|
res[line.id] = float(line.quantity) * line.amount * line.rate / 100
|
|
return res
|
|
|
|
_columns = {
|
|
'slip_id':fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade'),
|
|
'salary_rule_id':fields.many2one('hr.salary.rule', 'Rule', required=True),
|
|
'employee_id':fields.many2one('hr.employee', 'Employee', required=True),
|
|
'contract_id':fields.many2one('hr.contract', 'Contract', required=True, select=True),
|
|
'rate': fields.float('Rate (%)', digits_compute=dp.get_precision('Payroll Rate')),
|
|
'amount': fields.float('Amount', digits_compute=dp.get_precision('Payroll')),
|
|
'quantity': fields.float('Quantity', digits_compute=dp.get_precision('Payroll')),
|
|
'total': fields.function(_calculate_total, method=True, type='float', string='Total', digits_compute=dp.get_precision('Payroll'),store=True ),
|
|
}
|
|
|
|
_defaults = {
|
|
'quantity': 1.0,
|
|
'rate': 100.0,
|
|
}
|
|
|
|
|
|
|
|
class hr_employee(osv.osv):
|
|
'''
|
|
Employee
|
|
'''
|
|
|
|
_inherit = 'hr.employee'
|
|
_description = 'Employee'
|
|
|
|
def _calculate_total_wage(self, cr, uid, ids, name, args, context):
|
|
if not ids: return {}
|
|
res = {}
|
|
current_date = datetime.now().strftime('%Y-%m-%d')
|
|
for employee in self.browse(cr, uid, ids, context=context):
|
|
if not employee.contract_ids:
|
|
res[employee.id] = {'basic': 0.0}
|
|
continue
|
|
cr.execute( 'SELECT SUM(wage) '\
|
|
'FROM hr_contract '\
|
|
'WHERE employee_id = %s '\
|
|
'AND date_start <= %s '\
|
|
'AND (date_end > %s OR date_end is NULL)',
|
|
(employee.id, current_date, current_date))
|
|
result = dict(cr.dictfetchone())
|
|
res[employee.id] = {'basic': result['sum']}
|
|
return res
|
|
|
|
def _payslip_count(self, cr, uid, ids, field_name, arg, context=None):
|
|
Payslip = self.pool['hr.payslip']
|
|
return {
|
|
employee_id: Payslip.search_count(cr,uid, [('employee_id', '=', employee_id)], context=context)
|
|
for employee_id in ids
|
|
}
|
|
|
|
_columns = {
|
|
'slip_ids':fields.one2many('hr.payslip', 'employee_id', 'Payslips', required=False, readonly=True),
|
|
'total_wage': fields.function(_calculate_total_wage, method=True, type='float', string='Total Basic Salary', digits_compute=dp.get_precision('Payroll'), help="Sum of all current contract's wage of employee."),
|
|
'payslip_count': fields.function(_payslip_count, type='integer', string='Payslips'),
|
|
}
|
|
|
|
|
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|