2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU Affero General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU Affero General Public License for more details.
18 # You should have received a copy of the GNU Affero General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 ##############################################################################
24 from datetime import date
25 from datetime import datetime
26 from datetime import timedelta
27 from dateutil import relativedelta
30 from osv import fields, osv
32 from tools.translate import _
33 import decimal_precision as dp
35 from tools.safe_eval import safe_eval as eval
37 class hr_payroll_structure(osv.osv):
39 Salary structure used to defined
45 _name = 'hr.payroll.structure'
46 _description = 'Salary Structure'
48 'name':fields.char('Name', size=256, required=True),
49 'code':fields.char('Reference', size=64, required=True),
50 'company_id':fields.many2one('res.company', 'Company', required=True),
51 'note': fields.text('Description'),
52 'parent_id':fields.many2one('hr.payroll.structure', 'Parent'),
55 def _get_parent(self, cr, uid, context=None):
56 obj_model = self.pool.get('ir.model.data')
58 data_id = obj_model.search(cr, uid, [('model', '=', 'hr.payroll.structure'), ('name', '=', 'structure_base')])
60 res = obj_model.browse(cr, uid, data_id[0], context=context).res_id
64 'company_id': lambda self, cr, uid, context: \
65 self.pool.get('res.users').browse(cr, uid, uid,
66 context=context).company_id.id,
67 'parent_id': _get_parent,
70 def copy(self, cr, uid, id, default=None, context=None):
72 Create a new record in hr_payroll_structure model from existing one
73 @param cr: cursor to database
74 @param user: id of current user
75 @param id: list of record ids on which copy method executes
76 @param default: dict type contains the values to be override during copy of object
77 @param context: context arguments, like lang, time zone
79 @return: returns a id of newly created record
84 'code': self.browse(cr, uid, id, context=context).code + "(copy)",
85 'company_id': self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
87 return super(hr_payroll_structure, self).copy(cr, uid, id, default, context=context)
89 def get_all_rules(self, cr, uid, structure_ids, context=None):
91 @param structure_ids: list of structure
92 @return: returns a list of tuple (id, sequence) of rules that are maybe to apply
96 for struct in self.browse(cr, uid, structure_ids, context=context):
97 all_rules += self.pool.get('hr.salary.rule')._recursive_search_of_rules(cr, uid, struct.rule_ids, context=context)
100 def _get_parent_structure(self, cr, uid, struct_ids, context=None):
104 for struct in self.browse(cr, uid, struct_ids, context=context):
106 parent.append(struct.parent_id.id)
108 parent = self._get_parent_structure(cr, uid, parent, context)
109 return parent + struct_ids
111 hr_payroll_structure()
113 class hr_contract(osv.osv):
115 Employee contract based on the visa, work permits
116 allows to configure different Salary structure
119 _inherit = 'hr.contract'
120 _description = 'Employee Contract'
122 'struct_id': fields.many2one('hr.payroll.structure', 'Salary Structure', required=True),
123 'schedule_pay': fields.selection([
124 ('monthly', 'Monthly'),
125 ('quarterly', 'Quarterly'),
126 ('semi-annually', 'Semi-annually'),
127 ('annually', 'Annually'),
128 ('weekly', 'Weekly'),
129 ('bi-weekly', 'Bi-weekly'),
130 ('bi-monthly', 'Bi-monthly'),
131 ], 'Scheduled Pay', select=True),
134 def get_all_structures(self, cr, uid, contract_ids, context=None):
136 @param contract_ids: list of contracts
137 @return: the structures linked to the given contracts, ordered by hierachy (parent=False first, then first level children and so on) and without duplicata
140 structure_ids = [contract.struct_id.id for contract in self.browse(cr, uid, contract_ids, context=context)]
141 return list(set(self.pool.get('hr.payroll.structure')._get_parent_structure(cr, uid, structure_ids, context=context)))
145 class contrib_register(osv.osv):
147 Contribution Register
150 _name = 'hr.contribution.register'
151 _description = 'Contribution Register'
154 'company_id':fields.many2one('res.company', 'Company', required=False),
155 'name':fields.char('Name', size=256, required=True, readonly=False),
156 'register_line_ids':fields.one2many('hr.payslip.line', 'register_id', 'Register Line', readonly=True),
157 'note': fields.text('Description'),
160 'company_id': lambda self, cr, uid, context: \
161 self.pool.get('res.users').browse(cr, uid, uid,
162 context=context).company_id.id,
167 class hr_salary_category(osv.osv):
172 _name = 'hr.salary.category'
173 _description = 'Salary Category'
175 'name':fields.char('Name', size=64, required=True, readonly=False),
176 'code':fields.char('Code', size=64, required=True, readonly=False),
177 'parent_id':fields.many2one('hr.salary.category', 'Parent', help="Linking a salary category to its parent is used only for the reporting purpose."),
178 'note': fields.text('Description'),
179 'company_id':fields.many2one('res.company', 'Company', required=False),
180 'sequence': fields.integer('Sequence', required=True, help='Display sequence order'),
184 'company_id': lambda self, cr, uid, context: \
185 self.pool.get('res.users').browse(cr, uid, uid,
186 context=context).company_id.id,
192 class one2many_mod2(fields.one2many):
194 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
202 ids2 = obj.pool.get(self._obj).search(cr, user, [(self._fields_id,'in',ids), ('appears_on_payslip', '=', True)], limit=self._limit)
203 for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
204 res[r[self._fields_id]].append( r['id'] )
207 class hr_payslip(osv.osv):
213 _description = 'Pay Slip'
215 def _get_lines_salary_category(self, cr, uid, ids, field_names, arg=None, context=None):
217 if not ids: return result
218 cr.execute('''SELECT pl.slip_id, pl.id FROM hr_payslip_line AS pl \
219 LEFT JOIN hr_salary_category AS sh on (pl.category_id = sh.id) \
220 WHERE pl.slip_id in %s \
221 GROUP BY pl.slip_id, sh.sequence, pl.sequence, pl.id ORDER BY sh.sequence, pl.sequence''',(tuple(ids),))
224 result.setdefault(r[0], [])
225 result[r[0]].append(r[1])
229 'struct_id': fields.many2one('hr.payroll.structure', 'Structure', 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'),
230 'name': fields.char('Description', size=64, required=False, readonly=True, states={'draft': [('readonly', False)]}),
231 'number': fields.char('Reference', size=64, required=False, readonly=True, states={'draft': [('readonly', False)]}),
232 'employee_id': fields.many2one('hr.employee', 'Employee', required=True, readonly=True, states={'draft': [('readonly', False)]}),
233 'date_from': fields.date('Date From', readonly=True, states={'draft': [('readonly', False)]}, required=True),
234 'date_to': fields.date('Date To', readonly=True, states={'draft': [('readonly', False)]}, required=True),
235 'state': fields.selection([
236 ('draft', 'Waiting for Verification'),
237 ('hr_check', 'Waiting for HR Verification'),
238 ('accont_check', 'Waiting for Account Verification'),
239 ('confirm', 'Confirm Sheet'),
240 ('done', 'Paid Salary'),
241 ('cancel', 'Reject'),
242 ], 'State', select=True, readonly=True,
243 help=' * When the payslip is created the state is \'Waiting for verification\'.\
244 \n* It is varified by the user and payslip is sent for HR varification, the state is \'Waiting for HR Verification\'. \
245 \n* If HR varify the payslip, it is sent for account verification, the state is \'Waiting for Account Verification\'. \
246 \n* It is confirmed by the accountant and the state set to \'Confirm Sheet\'.\
247 \n* If the salary is paid then state is set to \'Paid Salary\'.\
248 \n* The \'Reject\' state is used when user cancel payslip.'),
249 # 'line_ids': fields.one2many('hr.payslip.line', 'slip_id', 'Payslip Line', required=False, readonly=True, states={'draft': [('readonly', False)]}),
250 'line_ids': one2many_mod2('hr.payslip.line', 'slip_id', 'Payslip Line',readonly=True, states={'draft':[('readonly',False)]}),
251 'company_id': fields.many2one('res.company', 'Company', required=False, readonly=True, states={'draft': [('readonly', False)]}),
252 'input_line_ids': fields.one2many('hr.payslip.input', 'payslip_id', 'Payslip Inputs', required=False, readonly=True, states={'draft': [('readonly', False)]}),
253 'paid': fields.boolean('Made Payment Order ? ', required=False, readonly=True, states={'draft': [('readonly', False)]}),
254 'note': fields.text('Description'),
255 'contract_id': fields.many2one('hr.contract', 'Contract', required=False, readonly=True, states={'draft': [('readonly', False)]}),
256 'details_by_salary_category': fields.function(_get_lines_salary_category, method=True, type='one2many', relation='hr.payslip.line', string='Details by Salary Head'),
257 'credit_note': fields.boolean('Credit Note', help="Indicates this payslip has a refund of another"),
260 'date_from': lambda *a: time.strftime('%Y-%m-01'),
261 'date_to': lambda *a: str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10],
263 'credit_note': False,
264 'company_id': lambda self, cr, uid, context: \
265 self.pool.get('res.users').browse(cr, uid, uid,
266 context=context).company_id.id,
269 def copy(self, cr, uid, id, default=None, context=None):
272 company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
277 'company_id': company_id,
279 'basic_before_leaves': 0.0,
282 return super(hr_payslip, self).copy(cr, uid, id, default, context=context)
284 def cancel_sheet(self, cr, uid, ids, context=None):
285 return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
287 def account_check_sheet(self, cr, uid, ids, context=None):
288 return self.write(cr, uid, ids, {'state': 'accont_check'}, context=context)
290 def hr_check_sheet(self, cr, uid, ids, context=None):
291 return self.write(cr, uid, ids, {'state': 'hr_check'}, context=context)
293 def process_sheet(self, cr, uid, ids, context=None):
294 return self.write(cr, uid, ids, {'paid': True, 'state': 'done'}, context=context)
296 def refund_sheet(self, cr, uid, ids, context=None):
297 mod_obj = self.pool.get('ir.model.data')
298 wf_service = netsvc.LocalService("workflow")
300 id_copy = self.copy(cr, uid, id, {'credit_note': True}, context=context)
301 self.compute_sheet(cr, uid, [id_copy], context=context)
302 wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'verify_sheet', cr)
303 wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'final_verify_sheet', cr)
304 wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'process_sheet', cr)
306 form_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_form')
307 form_res = form_id and form_id[1] or False
308 tree_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_tree')
309 tree_res = tree_id and tree_id[1] or False
311 'name':_("Refund Payslip"),
312 'view_mode': 'tree, form',
315 'res_model': 'hr.payslip',
316 'type': 'ir.actions.act_window',
319 'domain': "[('id', 'in', %s)]" % [id_copy],
320 'views': [(tree_res, 'tree'), (form_res, 'form')],
324 def verify_sheet(self, cr, uid, ids, context=None):
325 #TODO clean me: this function should create the register lines accordingly to the rules computed (run the compute_sheet first)
326 # holiday_pool = self.pool.get('hr.holidays')
327 # salary_rule_pool = self.pool.get('hr.salary.rule')
328 # structure_pool = self.pool.get('hr.payroll.structure')
329 # register_line_pool = self.pool.get('hr.contibution.register.line')
335 # for slip in self.browse(cr, uid, ids, context=context):
336 # if slip.contract_id:
337 # contracts.append(slip.contract_id)
339 # contracts = self.get_contract(cr, uid, slip.employee_id, slip.date, context=context)
340 # for contract in contracts:
341 # structures.append(contract.struct_id.id)
342 # leave_ids = self._get_leaves(cr, uid, slip.date, slip.employee_id, contract, context)
343 # for hday in holiday_pool.browse(cr, uid, leave_ids, context=context):
344 # salary_rules = salary_rule_pool.search(cr, uid, [('code', '=', hday.holiday_status_id.code)], context=context)
345 # rules += salary_rule_pool.browse(cr, uid, salary_rules, context=context)
346 # for structure in structures:
347 # sal_structures = self._get_parent_structure(cr, uid, [structure], context=context)
348 # for struct in sal_structures:
349 # lines = structure_pool.browse(cr, uid, struct, context=context).rule_ids
352 # for r in line.child_ids:
356 # 'basic': slip.basic_amount,
360 # if rule.company_contribution:
361 # base[rule.code.lower()] = rule.amount
362 # if rule.register_id:
363 # for slip in slip.line_ids:
364 # if slip.category_id == rule.category_id:
365 # line_tot = slip.total
366 # value = eval(rule.computational_expression, base)
367 # company_contrib = self._compute(cr, uid, rule.id, value, employee, contract, context)
370 # 'register_id': rule.register_id.id,
372 # 'employee_id': slip.employee_id.id,
373 # 'emp_deduction': line_tot,
374 # 'comp_deduction': company_contrib,
375 # 'total': rule.amount + line_tot
377 # register_line_pool.create(cr, uid, reg_line, context=context)
378 return self.write(cr, uid, ids, {'state': 'confirm'}, context=context)
380 #TODO move this function into hr_contract module, on hr.employee object
381 def get_contract(self, cr, uid, employee, date_from, date_to, context=None):
383 @param employee: browse record of employee
384 @param date_from: date field
385 @param date_to: date field
386 @return: returns the ids of all the contracts for the given employee that need to be considered for the given dates
388 contract_obj = self.pool.get('hr.contract')
390 #a contract is valid if it ends between the given dates
391 clause_1 = ['&',('date_end', '<=', date_to),('date_end','>=', date_from)]
392 #OR if it starts between the given dates
393 clause_2 = ['&',('date_start', '<=', date_to),('date_start','>=', date_from)]
394 #OR if it starts before the date_from and finish after the date_end (or never finish)
395 clause_3 = [('date_start','<=', date_from),'|',('date_end', '=', False),('date_end','>=', date)]
396 clause_final = [('employee_id', '=', employee.id),'|','|'] + clause_1 + clause_2 + clause_3
397 contract_ids = contract_obj.search(cr, uid, [('employee_id', '=', employee.id),], context=context)
400 def compute_sheet(self, cr, uid, ids, context=None):
401 slip_line_pool = self.pool.get('hr.payslip.line')
402 for payslip in self.browse(cr, uid, ids, context=context):
403 #delete old payslip lines
404 old_slipline_ids = slip_line_pool.search(cr, uid, [('slip_id', '=', payslip.id)], context=context)
407 slip_line_pool.unlink(cr, uid, old_slipline_ids, context=context)
408 if payslip.contract_id:
409 #set the list of contract for which the rules have to be applied
410 contract_ids = [payslip.contract_id.id]
412 #if we don't give the contract, then the rules to apply should be for all current contracts of the employee
413 contract_ids = self.get_contract(cr, uid, payslip.employee_id, payslip.date_from, payslip.date_to, context=context)
414 lines = [(0,0,line) for line in self.pool.get('hr.payslip').get_payslip_lines(cr, uid, contract_ids, payslip.id, context=context)]
415 self.write(cr, uid, [payslip.id], {'line_ids': lines}, context=context)
418 def get_input_lines(self, cr, uid, contract_ids, date_from, date_to, context=None):
420 @param contract_ids: list of contract id
421 @return: returns a list of dict containing the input that should be applied for the given contract between date_from and date_to
423 def was_on_leave(employee_id, datetime_day, context=None):
425 day = datetime_day.strftime("%Y-%m-%d")
426 holiday_ids = self.pool.get('hr.holidays').search(cr, uid, [('state','=','validate'),('employee_id','=',employee_id),('type','=','remove'),('date_from','<=',day),('date_to','>=',day)])
428 res = self.pool.get('hr.holidays').browse(cr, uid, holiday_ids, context=context)[0].holiday_status_id.name
432 for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
433 if not contract.working_hours:
434 #fill only if the contract as a working schedule linked
437 'name': _("Normal Working Days paid at 100%"),
440 'number_of_days': 0.0,
441 'number_of_hours': 0.0,
442 'contract_id': contract.id,
445 day_from = datetime.strptime(date_from,"%Y-%m-%d")
446 day_to = datetime.strptime(date_to,"%Y-%m-%d")
447 nb_of_days = (day_to - day_from).days + 1
448 for day in range(0, nb_of_days):
449 working_hours_on_day = self.pool.get('resource.calendar').working_hours_on_day(cr, uid, contract.working_hours, day_from + timedelta(days=day), context)
450 if working_hours_on_day:
451 #the employee had to work
452 leave_type = was_on_leave(contract.employee_id.id, day_from + timedelta(days=day), context=context)
454 #if he was on leave, fill the leaves dict
455 if leave_type in leaves:
456 leaves[leave_type]['number_of_days'] += 1.0
457 leaves[leave_type]['number_of_hours'] += working_hours_on_day
459 leaves[leave_type] = {
463 'number_of_days': 1.0,
464 'number_of_hours': working_hours_on_day,
465 'contract_id': contract.id,
468 #add the input vals to tmp (increment if existing)
469 attendances['number_of_days'] += 1.0
470 attendances['number_of_hours'] += working_hours_on_day
471 leaves = [value for key,value in leaves.items()]
472 res += [attendances] + leaves
475 def get_payslip_lines(self, cr, uid, contract_ids, payslip_id, context):
476 def _sum_salary_category(localdict, category, amount):
477 if category.parent_id:
478 localdict = _sum_salary_category(localdict, category.parent_id, amount)
479 localdict['categories'][category.code] = category.code in localdict['categories'] and localdict['categories'][category.code] + amount or amount
482 #we keep a dict with the result because a value can be overwritten by another rule with the same code
485 obj_rule = self.pool.get('hr.salary.rule')
486 payslip = self.pool.get('hr.payslip').browse(cr, uid, payslip_id, context=context)
488 for input_line in payslip.input_line_ids:
489 worked_days[input_line.code] = input_line
490 localdict = {'categories': {}, 'payslip': payslip, 'worked_days': worked_days}
491 #get the ids of the structures on the contracts and their parent id as well
492 structure_ids = self.pool.get('hr.contract').get_all_structures(cr, uid, contract_ids, context=context)
493 #get the rules of the structure and thier children
494 rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
495 #run the rules by sequence
496 sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
498 for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
499 employee = contract.employee_id
500 localdict.update({'employee': employee, 'contract': contract})
501 for rule in obj_rule.browse(cr, uid, sorted_rule_ids, context=context):
502 key = rule.code + '-' + str(contract.id)
503 localdict['result'] = None
504 #check if the rule can be applied
505 if obj_rule.satisfy_condition(cr, uid, rule.id, localdict, context=context) and rule.id not in blacklist:
506 #compute the amount of the rule
507 amount = obj_rule.compute_rule(cr, uid, rule.id, localdict, context=context)
508 #check if there is already a rule computed with that code
509 previous_amount = rule.code in localdict and localdict[rule.code] or 0.0
510 #set/overwrite the amount computed for this rule in the localdict
511 localdict[rule.code] = amount
512 #sum the amount for its salary head
513 localdict = _sum_salary_category(localdict, rule.category_id, amount - previous_amount)
514 #create/overwrite the rule in the temporary results
516 'salary_rule_id': rule.id,
517 'contract_id': contract.id,
520 'category_id': rule.category_id.id,
521 'sequence': rule.sequence,
522 'appears_on_payslip': rule.appears_on_payslip,
523 'condition_select': rule.condition_select,
524 'condition_python': rule.condition_python,
525 'condition_range': rule.condition_range,
526 'condition_range_min': rule.condition_range_min,
527 'condition_range_max': rule.condition_range_max,
528 'amount_select': rule.amount_select,
529 'amount_fix': rule.amount_fix,
530 'amount_python_compute': rule.amount_python_compute,
531 'amount_percentage': rule.amount_percentage,
532 'amount_percentage_base': rule.amount_percentage_base,
533 'register_id': rule.register_id.id,
535 'employee_id': contract.employee_id.id,
536 'quantity': rule.quantity,
539 #blacklist this rule and its children
540 blacklist += [id for id, seq in self.pool.get('hr.salary.rule')._recursive_search_of_rules(cr, uid, [rule], context=context)]
542 result = [value for code, value in result_dict.items()]
545 def onchange_employee_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
546 empolyee_obj = self.pool.get('hr.employee')
547 contract_obj = self.pool.get('hr.contract')
548 input_obj = self.pool.get('hr.payslip.input')
552 #delete old input lines
553 old_input_ids = ids and input_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
555 input_obj.unlink(cr, uid, old_input_ids, context=context)
560 #'details_by_salary_head':[], TODO put me back
562 'contract_id': False,
568 ttyme = datetime.fromtimestamp(time.mktime(time.strptime(date_from, "%Y-%m-%d")))
569 employee_id = empolyee_obj.browse(cr, uid, employee_id, context=context)
570 res['value'].update({
571 'name': _('Salary Slip of %s for %s') % (employee_id.name, tools.ustr(ttyme.strftime('%B-%Y'))),
572 'company_id': employee_id.company_id.id
575 if not context.get('contract', False):
576 #fill with the first contract of the employee
577 contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
578 res['value'].update({
579 'struct_id': contract_ids and contract_obj.read(cr, uid, contract_ids[0], ['struct_id'], context=context)['struct_id'][0] or False,
580 'contract_id': contract_ids and contract_ids[0] or False,
584 #set the list of contract for which the input have to be filled
585 contract_ids = [contract_id]
586 #fill the structure with the one on the selected contract
587 contract_record = contract_obj.browse(cr, uid, contract_id, context=context)
588 res['value'].update({'struct_id': contract_record.struct_id.id, 'contract_id': contract_id})
590 #if we don't give the contract, then the input to fill should be for all current contracts of the employee
591 contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
595 #computation of the salary input
596 input_line_ids = self.get_input_lines(cr, uid, contract_ids, date_from, date_to, context=context)
597 res['value'].update({
598 'input_line_ids': input_line_ids,
602 def onchange_contract_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
610 context.update({'contract': True})
612 res['value'].update({'struct_id': False})
613 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)
617 class hr_payslip_input(osv.osv):
622 _name = 'hr.payslip.input'
623 _description = 'Payslip Input'
625 'name': fields.char('Description', size=256, required=True),
626 'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True),
627 'sequence': fields.integer('Sequence', required=True,),
628 'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
629 'number_of_days': fields.float('Number of Days'),
630 'number_of_hours': fields.float('Number of Hours'),
631 'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
633 _order = 'payslip_id,sequence'
639 class hr_salary_rule(osv.osv):
641 _name = 'hr.salary.rule'
643 'name':fields.char('Name', size=256, required=True, readonly=False),
644 'code':fields.char('Code', size=64, required=True),
645 'sequence': fields.integer('Sequence', required=True, help='Use to arrange calculation sequence'),
646 'quantity': fields.char('Quantity', size=256, 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']."),
647 'category_id':fields.many2one('hr.salary.category', 'Category', required=True),
648 '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."),
649 'appears_on_payslip': fields.boolean('Appears on Payslip', help="Used for the display of rule on payslip"),
650 'parent_rule_id':fields.many2one('hr.salary.rule', 'Parent Salary Rule', select=True),
651 'company_id':fields.many2one('res.company', 'Company', required=False),
652 'condition_select': fields.selection([('none', 'Always True'),('range', 'Range'), ('python', 'Python Expression')], "Condition Based on", required=True),
653 'condition_range':fields.char('Range Based on',size=1024, readonly=False, help='This will use to computer the % fields values, in general its on basic, but You can use all heads code field in small letter as a variable name i.e. hra, ma, lta, etc...., also you can use, static varible basic'),#old name = conputional expression
654 '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.'),#old name = conditions
655 'condition_range_min': fields.float('Minimum Range', required=False, help="The minimum amount, applied for this rule."),
656 'condition_range_max': fields.float('Maximum Range', required=False, help="The maximum amount, applied for this rule."),
657 'amount_select':fields.selection([
658 ('percentage','Percentage (%)'),
659 ('fix','Fixed Amount'),
660 ('code','Python Code'),
661 ],'Amount Type', select=True, required=True, help="The computation method for the rule amount."),
662 'amount_fix': fields.float('Fixed Amount', digits_compute=dp.get_precision('Account'),),
663 'amount_percentage': fields.float('Percentage (%)', digits_compute=dp.get_precision('Account'), help='For example, enter 50.0 to apply a percentage of 50%'),
664 'amount_python_compute':fields.text('Python Code'),
665 'amount_percentage_base':fields.char('Percentage based on',size=1024, required=False, readonly=False, help='result will be affected to a variable'), #old name = expressiont
666 'child_ids':fields.one2many('hr.salary.rule', 'parent_rule_id', 'Child Salary Rule'),
667 'register_id':fields.property(
668 'hr.contribution.register',
670 relation='hr.contribution.register',
671 string="Contribution Register",
674 help="Contribution register based on company",
677 'note':fields.text('Description'),
680 'amount_python_compute': '''
681 # Available variables:
682 #----------------------
683 # payslip: hr.payslip object
684 # employee: hr.employee object
685 # contract: hr.contract object
686 # rules: rules code (previously computed)
687 # categories: dictionary containing the computed categories (sum of amount of all rules belonging to that category). Keys are the category codes.
688 # worked_days: dictionary containing the computed worked days. Keys are the worked days codes.
690 # Note: returned value have to be set in the variable 'result'
692 result = contract.wage * 0.10''',
695 # Available variables:
696 #----------------------
697 # payslip: hr.payslip object
698 # employee: hr.employee object
699 # contract: hr.contract object
700 # rules: rules code (previously computed)
701 # categories: dictionary containing the computed categories (sum of amount of all rules belonging to that category). Keys are the category codes.
702 # worked_days: dictionary containing the computed worked days. Keys are the worked days codes.
704 # Note: returned value have to be set in the variable 'result'
706 result = rules['NET'] > categories['NET'] * 0.10''',
707 'condition_range': 'contract.wage',
709 'appears_on_payslip': True,
711 'company_id': lambda self, cr, uid, context: \
712 self.pool.get('res.users').browse(cr, uid, uid,
713 context=context).company_id.id,
714 'condition_select': 'none',
715 'amount_select': 'fix',
717 'amount_percentage': 0.0,
721 def _recursive_search_of_rules(self, cr, uid, rule_ids, context=None):
723 @param rule_ids: list of browse record
724 @return: returns a list of tuple (id, sequence) which are all the children of the passed rule_ids
727 for rule in rule_ids:
729 children_rules += self._recursive_search_of_rules(cr, uid, rule.child_ids, context=context)
730 return [(r.id, r.sequence) for r in rule_ids] + children_rules
732 #TODO should add some checks on the type of result (should be float)
733 def compute_rule(self, cr, uid, rule_id, localdict, context=None):
735 @param rule_id: id of rule to compute
736 @param localdict: dictionary containing the environement in which to compute the rule
737 @return: returns the result of computation as float
739 rule = self.browse(cr, uid, rule_id, context=context)
740 if rule.amount_select == 'fix':
742 return rule.amount_fix * eval(rule.quantity, localdict)
744 raise osv.except_osv(_('Error'), _('Wrong quantity defined for salary rule %s (%s)')% (rule.name, rule.code))
745 elif rule.amount_select == 'percentage':
747 amount = rule.amount_percentage * eval(rule.amount_percentage_base, localdict) / 100
748 return amount * eval(rule.quantity, localdict)
750 raise osv.except_osv(_('Error'), _('Wrong percentage base or quantity defined for salary rule %s (%s)')% (rule.name, rule.code))
753 eval(rule.amount_python_compute, localdict, mode='exec', nocopy=True)
754 return localdict['result']
756 raise osv.except_osv(_('Error'), _('Wrong python code defined for salary rule %s (%s) ')% (rule.name, rule.code))
758 def satisfy_condition(self, cr, uid, rule_id, localdict, context=None):
760 @param rule_id: id of hr.salary.rule to be tested
761 @param contract_id: id of hr.contract to be tested
762 @return: returns True if the given rule match the condition for the given contract. Return False otherwise.
764 rule = self.browse(cr, uid, rule_id, context=context)
766 if rule.condition_select == 'none':
768 elif rule.condition_select == 'range':
770 result = eval(rule.condition_range, localdict)
771 return rule.condition_range_min <= result and result <= rule.condition_range_max or False
773 raise osv.except_osv(_('Error'), _('Wrong range condition defined for salary rule %s (%s)')% (rule.name, rule.code))
776 eval(rule.condition_python, localdict, mode='exec', nocopy=True)
777 return 'result' in localdict and localdict['result'] or False
779 raise osv.except_osv(_('Error'), _('Wrong python condition defined for salary rule %s (%s)')% (rule.name, rule.code))
783 class hr_payslip_line(osv.osv):
788 _name = 'hr.payslip.line'
789 _inherit = 'hr.salary.rule'
790 _description = 'Payslip Line'
791 _order = 'contract_id, sequence'
794 'slip_id':fields.many2one('hr.payslip', 'Pay Slip', required=True),
795 'salary_rule_id':fields.many2one('hr.salary.rule', 'Rule', required=True),
796 'employee_id':fields.many2one('hr.employee', 'Employee', required=True),
797 'contract_id':fields.many2one('hr.contract', 'Contract', required=True),
798 'total': fields.float('Amount', digits_compute=dp.get_precision('Account')),
799 'company_contrib': fields.float('Company Contribution', readonly=True, digits_compute=dp.get_precision('Account')),
804 class hr_payroll_structure(osv.osv):
806 _inherit = 'hr.payroll.structure'
808 'rule_ids':fields.many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id', 'Salary Rules'),
811 hr_payroll_structure()
813 class hr_employee(osv.osv):
818 _inherit = 'hr.employee'
819 _description = 'Employee'
821 def _calculate_total_wage(self, cr, uid, ids, name, args, context):
822 if not ids: return {}
824 current_date = datetime.now().strftime('%Y-%m-%d')
825 for employee in self.browse(cr, uid, ids, context=context):
826 if not employee.contract_ids:
827 res[employee.id] = {'basic': 0.0}
829 cr.execute( 'SELECT SUM(wage) '\
831 'WHERE employee_id = %s '\
832 'AND date_start <= %s '\
833 'AND (date_end > %s OR date_end is NULL)',
834 (employee.id, current_date, current_date))
835 result = dict(cr.dictfetchone())
836 res[employee.id] = {'basic': result['sum']}
840 'slip_ids':fields.one2many('hr.payslip', 'employee_id', 'Payslips', required=False, readonly=True),
841 'total_wage': fields.function(_calculate_total_wage, method=True, type='float', string='Total Basic Salary', digits_compute=dp.get_precision('Account'), help="Sum of all current contract's wage of employee."),
846 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: