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 'company_id': lambda self, cr, uid, context: \
56 self.pool.get('res.users').browse(cr, uid, uid,
57 context=context).company_id.id,
60 def copy(self, cr, uid, id, default=None, context=None):
62 Create a new record in hr_payroll_structure model from existing one
63 @param cr: cursor to database
64 @param user: id of current user
65 @param id: list of record ids on which copy method executes
66 @param default: dict type contains the values to be override during copy of object
67 @param context: context arguments, like lang, time zone
69 @return: returns a id of newly created record
74 'code': self.browse(cr, uid, id, context=context).code + "(copy)",
75 'company_id': self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
77 return super(hr_payroll_structure, self).copy(cr, uid, id, default, context=context)
79 def get_all_rules(self, cr, uid, structure_ids, context=None):
81 @param structure_ids: list of structure
82 @return: returns a list of tuple (id, sequence) of rules that are maybe to apply
86 for struct in self.browse(cr, uid, structure_ids, context=context):
87 all_rules += self.pool.get('hr.salary.rule')._recursive_search_of_rules(cr, uid, struct.rule_ids, context=context)
90 def _get_parent_structure(self, cr, uid, struct_ids, context=None):
94 for struct in self.browse(cr, uid, struct_ids, context=context):
96 parent.append(struct.parent_id.id)
98 parent = self._get_parent_structure(cr, uid, parent, context)
99 return parent + struct_ids
101 hr_payroll_structure()
103 class hr_contract(osv.osv):
105 Employee contract based on the visa, work permits
106 allows to configure different Salary structure
109 _inherit = 'hr.contract'
110 _description = 'Employee Contract'
112 'struct_id': fields.many2one('hr.payroll.structure', 'Salary Structure', required=True),
113 'schedule_pay': fields.selection([
114 ('monthly', 'Monthly'),
115 ('quarterly', 'Quarterly'),
116 ('semi-annually', 'Semi-annually'),
117 ('annually', 'Annually'),
118 ('weekly', 'Weekly'),
119 ('bi-weekly', 'Bi-weekly'),
120 ('bi-monthly', 'Bi-monthly'),
121 ], 'Scheduled Pay', select=True),
124 def get_all_structures(self, cr, uid, contract_ids, context=None):
126 @param contract_ids: list of contracts
127 @return: the structures linked to the given contracts, ordered by hierachy (parent=False first, then first level children and so on) and without duplicata
130 structure_ids = [contract.struct_id.id for contract in self.browse(cr, uid, contract_ids, context=context)]
131 return list(set(self.pool.get('hr.payroll.structure')._get_parent_structure(cr, uid, structure_ids, context=context)))
135 class contrib_register(osv.osv):
137 Contribution Register
140 _name = 'hr.contibution.register'
141 _description = 'Contribution Register'
144 'company_id':fields.many2one('res.company', 'Company', required=False),
145 'name':fields.char('Name', size=256, required=True, readonly=False),
146 'register_line_ids':fields.one2many('hr.payslip.line', 'register_id', 'Register Line', readonly=True),
147 'note': fields.text('Description'),
150 'company_id': lambda self, cr, uid, context: \
151 self.pool.get('res.users').browse(cr, uid, uid,
152 context=context).company_id.id,
157 class hr_salary_head(osv.osv):
162 _name = 'hr.salary.head'
163 _description = 'Salary Head'
165 'name':fields.char('Name', size=64, required=True, readonly=False),
166 'code':fields.char('Code', size=64, required=True, readonly=False),
167 'parent_id':fields.many2one('hr.salary.head', 'Parent', help="Linking a salary head to its parent is used only for the reporting purpose."),
168 'note': fields.text('Description'),
169 'company_id':fields.many2one('res.company', 'Company', required=False),
170 'sequence': fields.integer('Sequence', required=True, help='Display sequence order'),
174 'company_id': lambda self, cr, uid, context: \
175 self.pool.get('res.users').browse(cr, uid, uid,
176 context=context).company_id.id,
182 class hr_payslip(osv.osv):
188 _description = 'Pay Slip'
190 #TODO unused for now, cause the field is commented but we want to put it back
191 # def _get_salary_rules(self, cr, uid, ids, field_names, arg=None, context=None):
192 # structure_obj = self.pool.get('hr.payroll.structure')
193 # contract_obj = self.pool.get('hr.contract')
199 # sorted_salary_heads = []
200 # for record in self.browse(cr, uid, ids, context=context):
201 # if record.contract_id:
202 # contracts.append(record.contract_id.id)
204 # contracts = self.get_contract(cr, uid, record.employee_id, record.date, context=context)
205 # for contract in contracts:
206 # structures = contract_obj.get_all_structures(cr, uid, [contract], context)
207 # res[record.id] = {}
208 # for struct in structures:
209 # rule_ids = structure_obj.get_all_rules(cr, uid, [struct], context=None)
210 # for rl in rule_ids:
211 # if rl[0] not in rules:
212 # rules.append(rl[0])
213 # cr.execute('''SELECT sr.id FROM hr_salary_rule as sr, hr_salary_head as sh
214 # WHERE sr.category_id = sh.id AND sr.id in %s ORDER BY sh.sequence''',(tuple(rules),))
215 # for x in cr.fetchall():
216 # sorted_salary_heads.append(x[0])
217 # for fn in field_names:
218 # if fn == 'details_by_salary_head':
219 # res[record.id] = {fn: sorted_salary_heads}
223 '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'),
224 'name': fields.char('Description', size=64, required=False, readonly=True, states={'draft': [('readonly', False)]}),
225 'number': fields.char('Reference', size=64, required=False, readonly=True, states={'draft': [('readonly', False)]}),
226 'employee_id': fields.many2one('hr.employee', 'Employee', required=True, readonly=True, states={'draft': [('readonly', False)]}),
227 'date_from': fields.date('Date From', readonly=True, states={'draft': [('readonly', False)]}, required=True),
228 'date_to': fields.date('Date To', readonly=True, states={'draft': [('readonly', False)]}, required=True),
229 'state': fields.selection([
230 ('draft', 'Waiting for Verification'),
231 ('hr_check', 'Waiting for HR Verification'),
232 ('accont_check', 'Waiting for Account Verification'),
233 ('confirm', 'Confirm Sheet'),
234 ('done', 'Paid Salary'),
235 ('cancel', 'Reject'),
236 ], 'State', select=True, readonly=True,
237 help=' * When the payslip is created the state is \'Waiting for verification\'.\
238 \n* It is varified by the user and payslip is sent for HR varification, the state is \'Waiting for HR Verification\'. \
239 \n* If HR varify the payslip, it is sent for account verification, the state is \'Waiting for Account Verification\'. \
240 \n* It is confirmed by the accountant and the state set to \'Confirm Sheet\'.\
241 \n* If the salary is paid then state is set to \'Paid Salary\'.\
242 \n* The \'Reject\' state is used when user cancel payslip.'),
243 'line_ids': fields.one2many('hr.payslip.line', 'slip_id', 'Payslip Line', required=False, readonly=True, states={'draft': [('readonly', False)]}),
244 'company_id': fields.many2one('res.company', 'Company', required=False, readonly=True, states={'draft': [('readonly', False)]}),
245 'input_line_ids': fields.one2many('hr.payslip.input', 'payslip_id', 'Payslip Inputs', required=False, readonly=True, states={'draft': [('readonly', False)]}),
246 'paid': fields.boolean('Made Payment Order ? ', required=False, readonly=True, states={'draft': [('readonly', False)]}),
247 'note': fields.text('Description'),
248 'contract_id': fields.many2one('hr.contract', 'Contract', required=False, readonly=True, states={'draft': [('readonly', False)]}),
249 'credit_note': fields.boolean('Credit Note', help="It indicates that the payslip has been refunded", readonly=True),
251 # 'details_by_salary_head': fields.function(_get_salary_rules, method=True, type='one2many', relation='hr.salary.rule', string='Details by Salary Head', multi='details_by_salary_head'),
254 'date_from': lambda *a: time.strftime('%Y-%m-01'),
255 'date_to': lambda *a: str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10],
257 'credit_note': False,
258 'company_id': lambda self, cr, uid, context: \
259 self.pool.get('res.users').browse(cr, uid, uid,
260 context=context).company_id.id,
263 def copy(self, cr, uid, id, default=None, context=None):
266 company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
271 'move_payment_ids': [],
272 'company_id': company_id,
274 'basic_before_leaves': 0.0,
277 return super(hr_payslip, self).copy(cr, uid, id, default, context=context)
279 def cancel_sheet(self, cr, uid, ids, context=None):
280 return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
282 def account_check_sheet(self, cr, uid, ids, context=None):
283 return self.write(cr, uid, ids, {'state': 'accont_check'}, context=context)
285 def hr_check_sheet(self, cr, uid, ids, context=None):
286 return self.write(cr, uid, ids, {'state': 'hr_check'}, context=context)
288 def process_sheet(self, cr, uid, ids, context=None):
289 return self.write(cr, uid, ids, {'paid': True, 'state': 'done'}, context=context)
291 def refund_sheet(self, cr, uid, ids, context=None):
292 mod_obj = self.pool.get('ir.model.data')
293 wf_service = netsvc.LocalService("workflow")
295 id_copy = self.copy(cr, uid, id, {'credit_note': True}, context=context)
296 self.compute_sheet(cr, uid, [id_copy], context=context)
297 wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'verify_sheet', cr)
298 wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'final_verify_sheet', cr)
299 wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'process_sheet', cr)
301 form_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_form')
302 form_res = form_id and form_id[1] or False
303 tree_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_tree')
304 tree_res = tree_id and tree_id[1] or False
306 'name':_("Refund Payslip"),
307 'view_mode': 'tree, form',
310 'res_model': 'hr.payslip',
311 'type': 'ir.actions.act_window',
314 'domain': "[('id', 'in', %s)]" % [id_copy],
315 'views': [(tree_res, 'tree'), (form_res, 'form')],
319 def verify_sheet(self, cr, uid, ids, context=None):
320 #TODO clean me: this function should create the register lines accordingly to the rules computed (run the compute_sheet first)
321 # holiday_pool = self.pool.get('hr.holidays')
322 # salary_rule_pool = self.pool.get('hr.salary.rule')
323 # structure_pool = self.pool.get('hr.payroll.structure')
324 # register_line_pool = self.pool.get('hr.contibution.register.line')
330 # for slip in self.browse(cr, uid, ids, context=context):
331 # if slip.contract_id:
332 # contracts.append(slip.contract_id)
334 # contracts = self.get_contract(cr, uid, slip.employee_id, slip.date, context=context)
335 # for contract in contracts:
336 # structures.append(contract.struct_id.id)
337 # leave_ids = self._get_leaves(cr, uid, slip.date, slip.employee_id, contract, context)
338 # for hday in holiday_pool.browse(cr, uid, leave_ids, context=context):
339 # salary_rules = salary_rule_pool.search(cr, uid, [('code', '=', hday.holiday_status_id.code)], context=context)
340 # rules += salary_rule_pool.browse(cr, uid, salary_rules, context=context)
341 # for structure in structures:
342 # sal_structures = self._get_parent_structure(cr, uid, [structure], context=context)
343 # for struct in sal_structures:
344 # lines = structure_pool.browse(cr, uid, struct, context=context).rule_ids
347 # for r in line.child_ids:
351 # 'basic': slip.basic_amount,
355 # if rule.company_contribution:
356 # base[rule.code.lower()] = rule.amount
357 # if rule.register_id:
358 # for slip in slip.line_ids:
359 # if slip.category_id == rule.category_id:
360 # line_tot = slip.total
361 # value = eval(rule.computational_expression, base)
362 # company_contrib = self._compute(cr, uid, rule.id, value, employee, contract, context)
365 # 'register_id': rule.register_id.id,
367 # 'employee_id': slip.employee_id.id,
368 # 'emp_deduction': line_tot,
369 # 'comp_deduction': company_contrib,
370 # 'total': rule.amount + line_tot
372 # register_line_pool.create(cr, uid, reg_line, context=context)
373 return self.write(cr, uid, ids, {'state': 'confirm'}, context=context)
375 #TODO move this function into hr_contract module, on hr.employee object
376 def get_contract(self, cr, uid, employee, date_from, date_to, context=None):
378 @param employee: browse record of employee
379 @param date_from: date field
380 @param date_to: date field
381 @return: returns the ids of all the contracts for the given employee that need to be considered for the given dates
383 contract_obj = self.pool.get('hr.contract')
385 #a contract is valid if it ends between the given dates
386 clause_1 = ['&',('date_end', '<=', date_to),('date_end','>=', date_from)]
387 #OR if it starts between the given dates
388 clause_2 = ['&',('date_start', '<=', date_to),('date_start','>=', date_from)]
389 #OR if it starts before the date_from and finish after the date_end (or never finish)
390 clause_3 = [('date_start','<=', date_from),'|',('date_end', '=', False),('date_end','>=', date)]
391 clause_final = [('employee_id', '=', employee.id),'|','|'] + clause_1 + clause_2 + clause_3
392 contract_ids = contract_obj.search(cr, uid, [('employee_id', '=', employee.id),], context=context)
395 def compute_sheet(self, cr, uid, ids, context=None):
396 slip_line_pool = self.pool.get('hr.payslip.line')
397 for payslip in self.browse(cr, uid, ids, context=context):
398 #delete old payslip lines
399 old_slipline_ids = slip_line_pool.search(cr, uid, [('slip_id', '=', payslip.id)], context=context)
402 slip_line_pool.unlink(cr, uid, old_slipline_ids, context=context)
403 if payslip.contract_id:
404 #set the list of contract for which the rules have to be applied
405 contract_ids = [payslip.contract_id.id]
407 #if we don't give the contract, then the rules to apply should be for all current contracts of the employee
408 contract_ids = self.get_contract(cr, uid, payslip.employee_id, payslip.date_from, payslip.date_to, context=context)
409 lines = [(0,0,line) for line in self.pool.get('hr.payslip').get_payslip_lines(cr, uid, contract_ids, payslip.id, context=context)]
410 self.write(cr, uid, [payslip.id], {'line_ids': lines}, context=context)
413 def get_input_lines(self, cr, uid, contract_ids, date_from, date_to, context=None):
415 @param contract_ids: list of contract id
416 @return: returns a list of dict containing the input that should be applied for the given contract between date_from and date_to
418 def was_on_leave(employee_id, datetime_day, context=None):
420 day = datetime_day.strftime("%Y-%m-%d")
421 holiday_ids = self.pool.get('hr.holidays').search(cr, uid, [('state','=','validate'),('employee_id','=',employee_id),('type','=','remove'),('date_from','<=',day),('date_to','>=',day)])
423 res = self.pool.get('hr.holidays').browse(cr, uid, holiday_ids, context=context)[0].holiday_status_id.name
427 for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
428 if not contract.working_hours:
429 #fill only if the contract as a working schedule linked
432 'name': _("Normal Working Days paid at 100%"),
435 'number_of_days': 0.0,
436 'number_of_hours': 0.0,
437 'contract_id': contract.id,
440 day_from = datetime.strptime(date_from,"%Y-%m-%d")
441 day_to = datetime.strptime(date_to,"%Y-%m-%d")
442 nb_of_days = (day_to - day_from).days + 1
443 for day in range(0, nb_of_days):
444 working_hours_on_day = self.pool.get('resource.calendar').working_hours_on_day(cr, uid, contract.working_hours, day_from + timedelta(days=day), context)
445 if working_hours_on_day:
446 #the employee had to work
447 leave_type = was_on_leave(contract.employee_id.id, day_from + timedelta(days=day), context=context)
449 #if he was on leave, fill the leaves dict
450 if leave_type in leaves:
451 leaves[leave_type]['number_of_days'] += 1.0
452 leaves[leave_type]['number_of_hours'] += working_hours_on_day
454 leaves[leave_type] = {
458 'number_of_days': 1.0,
459 'number_of_hours': working_hours_on_day,
460 'contract_id': contract.id,
463 #add the input vals to tmp (increment if existing)
464 attendances['number_of_days'] += 1.0
465 attendances['number_of_hours'] += working_hours_on_day
466 leaves = [value for key,value in leaves.items()]
467 res += [attendances] + leaves
470 def get_payslip_lines(self, cr, uid, contract_ids, payslip_id, context):
471 def _sum_salary_head(localdict, head, amount):
473 localdict = _sum_salary_head(localdict, head.parent_id, amount)
474 localdict['heads'][head.code] = head.code in localdict['heads'] and localdict['heads'][head.code] + amount or amount
479 payslip = self.pool.get('hr.payslip').browse(cr, uid, payslip_id, context=context)
481 for input_line in payslip.input_line_ids:
482 worked_days[input_line.code] = input_line
483 localdict = {'rules': {}, 'heads': {}, 'payslip': payslip, 'worked_days': worked_days}
484 #get the ids of the structures on the contracts and their parent id as well
485 structure_ids = self.pool.get('hr.contract').get_all_structures(cr, uid, contract_ids, context=context)
486 #get the rules of the structure and thier children
487 rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
488 #run the rules by sequence
489 sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
491 for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
492 employee = contract.employee_id
493 localdict.update({'employee': employee, 'contract': contract})
494 for rule in self.pool.get('hr.salary.rule').browse(cr, uid, sorted_rule_ids, context=context):
495 localdict['result'] = None
496 #check if the rule can be applied
497 if self.pool.get('hr.salary.rule').satisfy_condition(cr, uid, rule.id, localdict, context=context) and rule.id not in blacklist:
498 amount = self.pool.get('hr.salary.rule').compute_rule(cr, uid, rule.id, localdict, context=context)
499 #set/overwrite the amount computed for this rule in the localdict
500 localdict['rules'][rule.code] = amount
501 #sum the amount for its salary head
502 localdict = _sum_salary_head(localdict, rule.category_id, amount)
504 'salary_rule_id': rule.id,
507 'category_id': rule.category_id.id,
508 'sequence': rule.sequence,
509 'appears_on_payslip': rule.appears_on_payslip,
510 'condition_select': rule.condition_select,
511 'condition_python': rule.condition_python,
512 'condition_range': rule.condition_range,
513 'condition_range_min': rule.condition_range_min,
514 'condition_range_max': rule.condition_range_max,
515 'amount_select': rule.amount_select,
516 'amount_fix': rule.amount_fix,
517 'amount_python_compute': rule.amount_python_compute,
518 'amount_percentage': rule.amount_percentage,
519 'amount_percentage_base': rule.amount_percentage_base,
520 'register_id': rule.register_id.id,
522 'employee_id': contract.employee_id.id,
526 #blacklist this rule and its children
527 blacklist += [id for id, seq in self.pool.get('hr.salary.rule')._recursive_search_of_rules(cr, uid, [rule], context=context)]
530 def onchange_employee_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
531 empolyee_obj = self.pool.get('hr.employee')
532 contract_obj = self.pool.get('hr.contract')
533 input_obj = self.pool.get('hr.payslip.input')
537 #delete old input lines
538 old_input_ids = ids and input_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
540 input_obj.unlink(cr, uid, old_input_ids, context=context)
545 #'details_by_salary_head':[], TODO put me back
547 'contract_id': False,
553 ttyme = datetime.fromtimestamp(time.mktime(time.strptime(date_from, "%Y-%m-%d")))
554 employee_id = empolyee_obj.browse(cr, uid, employee_id, context=context)
555 res['value'].update({
556 'name': _('Salary Slip of %s for %s') % (employee_id.name, tools.ustr(ttyme.strftime('%B-%Y'))),
557 'company_id': employee_id.company_id.id
560 if not context.get('contract', False):
561 #fill with the first contract of the employee
562 contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
563 res['value'].update({
564 'struct_id': contract_ids and contract_obj.read(cr, uid, contract_ids[0], ['struct_id'], context=context)['struct_id'][0] or False,
565 'contract_id': contract_ids and contract_ids[0] or False,
569 #set the list of contract for which the input have to be filled
570 contract_ids = [contract_id]
571 #fill the structure with the one on the selected contract
572 contract_record = contract_obj.browse(cr, uid, contract_id, context=context)
573 res['value'].update({'struct_id': contract_record.struct_id.id, 'contract_id': contract_id})
575 #if we don't give the contract, then the input to fill should be for all current contracts of the employee
576 contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
580 #computation of the salary input
581 input_line_ids = self.get_input_lines(cr, uid, contract_ids, date_from, date_to, context=context)
582 res['value'].update({
583 'input_line_ids': input_line_ids,
587 def onchange_contract_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
595 context.update({'contract': True})
597 res['value'].update({'struct_id': False})
598 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)
602 class hr_payslip_input(osv.osv):
607 _name = 'hr.payslip.input'
608 _description = 'Payslip Input'
610 'name': fields.char('Description', size=256, required=True),
611 'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True),
612 'sequence': fields.integer('Sequence', required=True,),
613 'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
614 'number_of_days': fields.float('Number of Days'),
615 'number_of_hours': fields.float('Number of Hours'),
616 'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
618 _order = 'payslip_id,sequence'
624 class hr_salary_rule(osv.osv):
626 _name = 'hr.salary.rule'
628 'name':fields.char('Name', size=256, required=True, readonly=False),
629 'code':fields.char('Code', size=64, required=True),
630 'sequence': fields.integer('Sequence', required=True, help='Use to arrange calculation sequence'),
631 'category_id':fields.many2one('hr.salary.head', 'Salary Head', required=True),
632 '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."),
633 'appears_on_payslip': fields.boolean('Appears on Payslip', help="Used for the display of rule on payslip"),
634 'parent_rule_id':fields.many2one('hr.salary.rule', 'Parent Salary Rule', select=True),
635 'company_id':fields.many2one('res.company', 'Company', required=False),
636 'condition_select': fields.selection([('none', 'Always True'),('range', 'Range'), ('python', 'Python Expression')], "Condition Based on", required=True),
637 '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
638 '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
639 'condition_range_min': fields.float('Minimum Range', required=False, help="The minimum amount, applied for this rule."),
640 'condition_range_max': fields.float('Maximum Range', required=False, help="The maximum amount, applied for this rule."),
641 'amount_select':fields.selection([
642 ('percentage','Percentage (%)'),
643 ('fix','Fixed Amount'),
644 ('code','Python Code'),
645 ],'Amount Type', select=True, required=True, help="The computation method for the rule amount."),
646 'amount_fix': fields.float('Fixed Amount', digits_compute=dp.get_precision('Account'),),
647 'amount_percentage': fields.float('Percentage (%)', digits_compute=dp.get_precision('Account'), help='For example, enter 50.0 to apply a percentage of 50%'),
648 'amount_python_compute':fields.text('Python Code'),
649 '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
650 'child_ids':fields.one2many('hr.salary.rule', 'parent_rule_id', 'Child Salary Rule'),
651 'register_id':fields.property(
652 'hr.contibution.register',
654 relation='hr.contibution.register',
655 string="Contribution Register",
658 help="Contribution register based on company",
661 'note':fields.text('Description'),
664 'amount_python_compute': '''
665 # Available variables:
666 #----------------------
667 # payslip: hr.payslip object
668 # employee: hr.employee object
669 # contract: hr.contract object
670 # rules: dictionary containing the previsouly computed rules. Keys are the rule codes.
671 # heads: dictionary containing the computed heads (sum of amount of all rules belonging to that head). Keys are the head codes.
672 # worked_days: dictionary containing the computed worked days. Keys are the worked days codes.
674 # Note: returned value have to be set in the variable 'result'
676 result = contract.wage * 0.10''',
679 # Available variables:
680 #----------------------
681 # payslip: hr.payslip object
682 # employee: hr.employee object
683 # contract: hr.contract object
684 # rules: dictionary containing the previsouly computed rules. Keys are the rule codes.
685 # heads: dictionary containing the computed heads (sum of amount of all rules belonging to that head). Keys are the head codes.
686 # worked_days: dictionary containing the computed worked days. Keys are the worked days codes.
688 # Note: returned value have to be set in the variable 'result'
690 result = rules['NET'] > heads['NET'] * 0.10''',
691 'condition_range': 'contract.wage',
693 'appears_on_payslip': True,
695 'company_id': lambda self, cr, uid, context: \
696 self.pool.get('res.users').browse(cr, uid, uid,
697 context=context).company_id.id,
698 'condition_select': 'none',
699 'amount_select': 'fix',
701 'amount_percentage': 0.0,
704 def _recursive_search_of_rules(self, cr, uid, rule_ids, context=None):
706 @param rule_ids: list of browse record
707 @return: returns a list of tuple (id, sequence) which are all the children of the passed rule_ids
710 for rule in rule_ids:
712 children_rules += self._recursive_search_of_rules(cr, uid, rule.child_ids, context=context)
713 return [(r.id, r.sequence) for r in rule_ids] + children_rules
715 #TODO should add some checks on the type of result (should be float)
716 def compute_rule(self, cr, uid, rule_id, localdict, context=None):
718 @param rule_id: id of rule to compute
719 @param localdict: dictionary containing the environement in which to compute the rule
720 @return: returns the result of computation as float
722 rule = self.browse(cr, uid, rule_id, context=context)
723 if rule.amount_select == 'fix':
724 return rule.amount_fix
725 elif rule.amount_select == 'percentage':
727 return rule.amount_percentage * eval(rule.amount_percentage_base, localdict) / 100
729 raise osv.except_osv(_('Error'), _('Wrong percentage base defined for salary rule %s (%s)')% (rule.name, rule.code))
732 eval(rule.amount_python_compute, localdict, mode='exec', nocopy=True)
733 return localdict['result']
735 raise osv.except_osv(_('Error'), _('Wrong python code defined for salary rule %s (%s) ')% (rule.name, rule.code))
737 def satisfy_condition(self, cr, uid, rule_id, localdict, context=None):
739 @param rule_id: id of hr.salary.rule to be tested
740 @param contract_id: id of hr.contract to be tested
741 @return: returns True if the given rule match the condition for the given contract. Return False otherwise.
743 rule = self.browse(cr, uid, rule_id, context=context)
745 if rule.condition_select == 'none':
747 elif rule.condition_select == 'range':
749 result = eval(rule.condition_range, localdict)
750 return rule.condition_range_min <= result and result <= rule.condition_range_max or False
752 raise osv.except_osv(_('Error'), _('Wrong range condition defined for salary rule %s (%s)')% (rule.name, rule.code))
755 eval(rule.condition_python, localdict, mode='exec', nocopy=True)
756 return 'result' in localdict and localdict['result'] or False
758 raise osv.except_osv(_('Error'), _('Wrong python condition defined for salary rule %s (%s)')% (rule.name, rule.code))
762 class hr_payslip_line(osv.osv):
767 _name = 'hr.payslip.line'
768 _inherit = 'hr.salary.rule'
769 _description = 'Payslip Line'
773 'slip_id':fields.many2one('hr.payslip', 'Pay Slip', required=True),
774 'salary_rule_id':fields.many2one('hr.salary.rule', 'Rule', required=True),
775 'employee_id':fields.many2one('hr.employee', 'Employee', required=True),
776 'total': fields.float('Amount', digits_compute=dp.get_precision('Account')),
777 'company_contrib': fields.float('Company Contribution', readonly=True, digits_compute=dp.get_precision('Account')),
782 class hr_payroll_structure(osv.osv):
784 _inherit = 'hr.payroll.structure'
786 'rule_ids':fields.many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id', 'Salary Rules'),
789 hr_payroll_structure()
791 class hr_employee(osv.osv):
796 _inherit = 'hr.employee'
797 _description = 'Employee'
799 def _calculate_total_wage(self, cr, uid, ids, name, args, context):
800 if not ids: return {}
802 current_date = datetime.now().strftime('%Y-%m-%d')
803 for employee in self.browse(cr, uid, ids, context=context):
804 if not employee.contract_ids:
805 res[employee.id] = {'basic': 0.0}
807 cr.execute( 'SELECT SUM(wage) '\
809 'WHERE employee_id = %s '\
810 'AND date_start <= %s '\
811 'AND (date_end > %s OR date_end is NULL)',
812 (employee.id, current_date, current_date))
813 result = dict(cr.dictfetchone())
814 res[employee.id] = {'basic': result['sum']}
818 'slip_ids':fields.one2many('hr.payslip', 'employee_id', 'Payslips', required=False, readonly=True),
819 '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."),
824 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: