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'),
53 'children_ids':fields.one2many('hr.payroll.structure', 'parent_id', 'Children'),
56 def _get_parent(self, cr, uid, context=None):
57 obj_model = self.pool.get('ir.model.data')
59 data_id = obj_model.search(cr, uid, [('model', '=', 'hr.payroll.structure'), ('name', '=', 'structure_base')])
61 res = obj_model.browse(cr, uid, data_id[0], context=context).res_id
65 'company_id': lambda self, cr, uid, context: \
66 self.pool.get('res.users').browse(cr, uid, uid,
67 context=context).company_id.id,
68 'parent_id': _get_parent,
71 def copy(self, cr, uid, id, default=None, context=None):
73 Create a new record in hr_payroll_structure model from existing one
74 @param cr: cursor to database
75 @param user: id of current user
76 @param id: list of record ids on which copy method executes
77 @param default: dict type contains the values to be override during copy of object
78 @param context: context arguments, like lang, time zone
80 @return: returns a id of newly created record
85 'code': self.browse(cr, uid, id, context=context).code + "(copy)",
86 'company_id': self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
88 return super(hr_payroll_structure, self).copy(cr, uid, id, default, context=context)
90 def get_all_rules(self, cr, uid, structure_ids, context=None):
92 @param structure_ids: list of structure
93 @return: returns a list of tuple (id, sequence) of rules that are maybe to apply
97 for struct in self.browse(cr, uid, structure_ids, context=context):
98 all_rules += self.pool.get('hr.salary.rule')._recursive_search_of_rules(cr, uid, struct.rule_ids, context=context)
101 def _get_parent_structure(self, cr, uid, struct_ids, context=None):
105 for struct in self.browse(cr, uid, struct_ids, context=context):
107 parent.append(struct.parent_id.id)
109 parent = self._get_parent_structure(cr, uid, parent, context)
110 return parent + struct_ids
112 hr_payroll_structure()
114 class hr_contract(osv.osv):
116 Employee contract based on the visa, work permits
117 allows to configure different Salary structure
120 _inherit = 'hr.contract'
121 _description = 'Employee Contract'
123 'struct_id': fields.many2one('hr.payroll.structure', 'Salary Structure'),
124 'schedule_pay': fields.selection([
125 ('monthly', 'Monthly'),
126 ('quarterly', 'Quarterly'),
127 ('semi-annually', 'Semi-annually'),
128 ('annually', 'Annually'),
129 ('weekly', 'Weekly'),
130 ('bi-weekly', 'Bi-weekly'),
131 ('bi-monthly', 'Bi-monthly'),
132 ], 'Scheduled Pay', select=True),
136 'schedule_pay': 'monthly',
139 def get_all_structures(self, cr, uid, contract_ids, context=None):
141 @param contract_ids: list of contracts
142 @return: the structures linked to the given contracts, ordered by hierachy (parent=False first, then first level children and so on) and without duplicata
145 structure_ids = [contract.struct_id.id for contract in self.browse(cr, uid, contract_ids, context=context)]
146 return list(set(self.pool.get('hr.payroll.structure')._get_parent_structure(cr, uid, structure_ids, context=context)))
150 class contrib_register(osv.osv):
152 Contribution Register
155 _name = 'hr.contribution.register'
156 _description = 'Contribution Register'
159 'company_id':fields.many2one('res.company', 'Company', required=False),
160 'name':fields.char('Name', size=256, required=True, readonly=False),
161 'register_line_ids':fields.one2many('hr.payslip.line', 'register_id', 'Register Line', readonly=True),
162 'note': fields.text('Description'),
165 'company_id': lambda self, cr, uid, context: \
166 self.pool.get('res.users').browse(cr, uid, uid,
167 context=context).company_id.id,
172 class hr_salary_rule_category(osv.osv):
174 HR Salary Rule Category
177 _name = 'hr.salary.rule.category'
178 _description = 'Salary Rule Category'
180 'name':fields.char('Name', size=64, required=True, readonly=False),
181 'code':fields.char('Code', size=64, required=True, readonly=False),
182 'parent_id':fields.many2one('hr.salary.rule.category', 'Parent', help="Linking a salary category to its parent is used only for the reporting purpose."),
183 'children_ids': fields.one2many('hr.salary.rule.category', 'parent_id', 'Children'),
184 'note': fields.text('Description'),
185 'company_id':fields.many2one('res.company', 'Company', required=False),
189 'company_id': lambda self, cr, uid, context: \
190 self.pool.get('res.users').browse(cr, uid, uid,
191 context=context).company_id.id,
194 hr_salary_rule_category()
196 class one2many_mod2(fields.one2many):
198 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
206 ids2 = obj.pool.get(self._obj).search(cr, user, [(self._fields_id,'in',ids), ('appears_on_payslip', '=', True)], limit=self._limit)
207 for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
208 res[r[self._fields_id]].append( r['id'] )
211 class hr_payslip_run(osv.osv):
213 _name = 'hr.payslip.run'
214 _description = 'Payslip Run'
216 'name': fields.char('Name', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
217 'slip_ids': fields.one2many('hr.payslip', 'payslip_run_id', 'Payslips', required=False, readonly=True, states={'draft': [('readonly', False)]}),
218 'state': fields.selection([
221 ], 'State', select=True, readonly=True),
222 'date_start': fields.date('Date From', required=True, readonly=True, states={'draft': [('readonly', False)]}),
223 'date_end': fields.date('Date To', required=True, readonly=True, states={'draft': [('readonly', False)]}),
224 '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."),
228 'date_start': lambda *a: time.strftime('%Y-%m-01'),
229 'date_end': lambda *a: str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10],
232 def draft_payslip_run(self, cr, uid, ids, context=None):
233 return self.write(cr, uid, ids, {'state': 'draft'}, context=context)
235 def close_payslip_run(self, cr, uid, ids, context=None):
236 return self.write(cr, uid, ids, {'state': 'close'}, context=context)
240 class hr_payslip(osv.osv):
246 _description = 'Pay Slip'
248 def _get_lines_salary_rule_category(self, cr, uid, ids, field_names, arg=None, context=None):
250 if not ids: return result
252 result.setdefault(id, [])
253 cr.execute('''SELECT pl.slip_id, pl.id FROM hr_payslip_line AS pl \
254 LEFT JOIN hr_salary_rule_category AS sh on (pl.category_id = sh.id) \
255 WHERE pl.slip_id in %s \
256 GROUP BY pl.slip_id, pl.sequence, pl.id ORDER BY pl.sequence''',(tuple(ids),))
259 result[r[0]].append(r[1])
263 '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'),
264 'name': fields.char('Description', size=64, required=False, readonly=True, states={'draft': [('readonly', False)]}),
265 'number': fields.char('Reference', size=64, required=False, readonly=True, states={'draft': [('readonly', False)]}),
266 'employee_id': fields.many2one('hr.employee', 'Employee', required=True, readonly=True, states={'draft': [('readonly', False)]}),
267 'date_from': fields.date('Date From', readonly=True, states={'draft': [('readonly', False)]}, required=True),
268 'date_to': fields.date('Date To', readonly=True, states={'draft': [('readonly', False)]}, required=True),
269 'state': fields.selection([
271 ('verify', 'Waiting'),
273 ('cancel', 'Rejected'),
274 ], 'State', select=True, readonly=True,
275 help='* When the payslip is created the state is \'Draft\'.\
276 \n* If the payslip is under verification, the state is \'Waiting\'. \
277 \n* If the payslip is confirmed then state is set to \'Done\'.\
278 \n* When user cancel payslip the state is \'Rejected\'.'),
279 # 'line_ids': fields.one2many('hr.payslip.line', 'slip_id', 'Payslip Line', required=False, readonly=True, states={'draft': [('readonly', False)]}),
280 'line_ids': one2many_mod2('hr.payslip.line', 'slip_id', 'Payslip Lines', readonly=True, states={'draft':[('readonly',False)]}),
281 'company_id': fields.many2one('res.company', 'Company', required=False, readonly=True, states={'draft': [('readonly', False)]}),
282 'worked_days_line_ids': fields.one2many('hr.payslip.worked_days', 'payslip_id', 'Payslip Worked Days', required=False, readonly=True, states={'draft': [('readonly', False)]}),
283 'input_line_ids': fields.one2many('hr.payslip.input', 'payslip_id', 'Payslip Inputs', required=False, readonly=True, states={'draft': [('readonly', False)]}),
284 'paid': fields.boolean('Made Payment Order ? ', required=False, readonly=True, states={'draft': [('readonly', False)]}),
285 'note': fields.text('Description', readonly=True, states={'draft':[('readonly',False)]}),
286 'contract_id': fields.many2one('hr.contract', 'Contract', required=False, readonly=True, states={'draft': [('readonly', False)]}),
287 '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'),
288 'credit_note': fields.boolean('Credit Note', help="Indicates this payslip has a refund of another", readonly=True, states={'draft': [('readonly', False)]}),
289 'payslip_run_id': fields.many2one('hr.payslip.run', 'Payslip Run', readonly=True, states={'draft': [('readonly', False)]}),
292 'date_from': lambda *a: time.strftime('%Y-%m-01'),
293 'date_to': lambda *a: str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10],
295 'credit_note': False,
296 'company_id': lambda self, cr, uid, context: \
297 self.pool.get('res.users').browse(cr, uid, uid,
298 context=context).company_id.id,
301 def copy(self, cr, uid, id, default=None, context=None):
304 company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
309 'company_id': company_id,
311 'basic_before_leaves': 0.0,
314 return super(hr_payslip, self).copy(cr, uid, id, default, context=context)
316 def cancel_sheet(self, cr, uid, ids, context=None):
317 return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
319 def process_sheet(self, cr, uid, ids, context=None):
320 return self.write(cr, uid, ids, {'paid': True, 'state': 'done'}, context=context)
322 def hr_verify_sheet(self, cr, uid, ids, context=None):
323 return self.write(cr, uid, ids, {'state': 'verify'}, context=context)
325 def refund_sheet(self, cr, uid, ids, context=None):
326 mod_obj = self.pool.get('ir.model.data')
327 wf_service = netsvc.LocalService("workflow")
328 for payslip in self.browse(cr, uid, ids, context=context):
329 id_copy = self.copy(cr, uid, payslip.id, {'credit_note': True, 'name': _('Refund: ')+payslip.name}, context=context)
330 self.compute_sheet(cr, uid, [id_copy], context=context)
331 wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'hr_verify_sheet', cr)
332 wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'process_sheet', cr)
334 form_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_form')
335 form_res = form_id and form_id[1] or False
336 tree_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_tree')
337 tree_res = tree_id and tree_id[1] or False
339 'name':_("Refund Payslip"),
340 'view_mode': 'tree, form',
343 'res_model': 'hr.payslip',
344 'type': 'ir.actions.act_window',
347 'domain': "[('id', 'in', %s)]" % [id_copy],
348 'views': [(tree_res, 'tree'), (form_res, 'form')],
352 def check_done(self, cr, uid, ids, context=None):
355 #TODO move this function into hr_contract module, on hr.employee object
356 def get_contract(self, cr, uid, employee, date_from, date_to, context=None):
358 @param employee: browse record of employee
359 @param date_from: date field
360 @param date_to: date field
361 @return: returns the ids of all the contracts for the given employee that need to be considered for the given dates
363 contract_obj = self.pool.get('hr.contract')
365 #a contract is valid if it ends between the given dates
366 clause_1 = ['&',('date_end', '<=', date_to),('date_end','>=', date_from)]
367 #OR if it starts between the given dates
368 clause_2 = ['&',('date_start', '<=', date_to),('date_start','>=', date_from)]
369 #OR if it starts before the date_from and finish after the date_end (or never finish)
370 clause_3 = [('date_start','<=', date_from),'|',('date_end', '=', False),('date_end','>=', date_to)]
371 clause_final = [('employee_id', '=', employee.id),'|','|'] + clause_1 + clause_2 + clause_3
372 contract_ids = contract_obj.search(cr, uid, clause_final, context=context)
375 def compute_sheet(self, cr, uid, ids, context=None):
376 slip_line_pool = self.pool.get('hr.payslip.line')
377 sequence_obj = self.pool.get('ir.sequence')
378 for payslip in self.browse(cr, uid, ids, context=context):
379 number = payslip.number or sequence_obj.get(cr, uid, 'salary.slip')
380 #delete old payslip lines
381 old_slipline_ids = slip_line_pool.search(cr, uid, [('slip_id', '=', payslip.id)], context=context)
384 slip_line_pool.unlink(cr, uid, old_slipline_ids, context=context)
385 if payslip.contract_id:
386 #set the list of contract for which the rules have to be applied
387 contract_ids = [payslip.contract_id.id]
389 #if we don't give the contract, then the rules to apply should be for all current contracts of the employee
390 contract_ids = self.get_contract(cr, uid, payslip.employee_id, payslip.date_from, payslip.date_to, context=context)
391 lines = [(0,0,line) for line in self.pool.get('hr.payslip').get_payslip_lines(cr, uid, contract_ids, payslip.id, context=context)]
392 self.write(cr, uid, [payslip.id], {'line_ids': lines, 'number': number,}, context=context)
395 def get_worked_day_lines(self, cr, uid, contract_ids, date_from, date_to, context=None):
397 @param contract_ids: list of contract id
398 @return: returns a list of dict containing the input that should be applied for the given contract between date_from and date_to
400 def was_on_leave(employee_id, datetime_day, context=None):
402 day = datetime_day.strftime("%Y-%m-%d")
403 holiday_ids = self.pool.get('hr.holidays').search(cr, uid, [('state','=','validate'),('employee_id','=',employee_id),('type','=','remove'),('date_from','<=',day),('date_to','>=',day)])
405 res = self.pool.get('hr.holidays').browse(cr, uid, holiday_ids, context=context)[0].holiday_status_id.name
409 for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
410 if not contract.working_hours:
411 #fill only if the contract as a working schedule linked
414 'name': _("Normal Working Days paid at 100%"),
417 'number_of_days': 0.0,
418 'number_of_hours': 0.0,
419 'contract_id': contract.id,
422 day_from = datetime.strptime(date_from,"%Y-%m-%d")
423 day_to = datetime.strptime(date_to,"%Y-%m-%d")
424 nb_of_days = (day_to - day_from).days + 1
425 for day in range(0, nb_of_days):
426 working_hours_on_day = self.pool.get('resource.calendar').working_hours_on_day(cr, uid, contract.working_hours, day_from + timedelta(days=day), context)
427 if working_hours_on_day:
428 #the employee had to work
429 leave_type = was_on_leave(contract.employee_id.id, day_from + timedelta(days=day), context=context)
431 #if he was on leave, fill the leaves dict
432 if leave_type in leaves:
433 leaves[leave_type]['number_of_days'] += 1.0
434 leaves[leave_type]['number_of_hours'] += working_hours_on_day
436 leaves[leave_type] = {
440 'number_of_days': 1.0,
441 'number_of_hours': working_hours_on_day,
442 'contract_id': contract.id,
445 #add the input vals to tmp (increment if existing)
446 attendances['number_of_days'] += 1.0
447 attendances['number_of_hours'] += working_hours_on_day
448 leaves = [value for key,value in leaves.items()]
449 res += [attendances] + leaves
452 def get_inputs(self, cr, uid, contract_ids, date_from, date_to, context=None):
454 contract_obj = self.pool.get('hr.contract')
455 rule_obj = self.pool.get('hr.salary.rule')
457 structure_ids = contract_obj.get_all_structures(cr, uid, contract_ids, context=context)
458 rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
459 sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
461 for contract in contract_obj.browse(cr, uid, contract_ids, context=context):
462 for rule in rule_obj.browse(cr, uid, sorted_rule_ids, context=context):
464 for input in rule.input_ids:
468 'contract_id': contract.id,
473 def get_payslip_lines(self, cr, uid, contract_ids, payslip_id, context):
474 def _sum_salary_rule_category(localdict, category, amount):
475 if category.parent_id:
476 localdict = _sum_salary_rule_category(localdict, category.parent_id, amount)
477 localdict['categories'].dict[category.code] = category.code in localdict['categories'].dict and localdict['categories'].dict[category.code] + amount or amount
480 class BrowsableObject(object):
481 def __init__(self, pool, cr, uid, employee_id, dict):
485 self.employee_id = employee_id
488 def __getattr__(self, attr):
489 return attr in self.dict and self.dict.__getitem__(attr) or 0.0
491 class InputLine(BrowsableObject):
492 """a class that will be used into the python code, mainly for usability purposes"""
493 def sum(self, code, from_date, to_date=None):
495 to_date = datetime.now().strftime('%Y-%m-%d')
497 self.cr.execute("SELECT sum(amount) as sum\
498 FROM hr_payslip as hp, hr_payslip_input as pi \
499 WHERE hp.employee_id = %s AND hp.state = 'done' \
500 AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s",
501 (self.employee_id, from_date, to_date, code))
502 res = self.cr.fetchone()[0]
505 class WorkedDays(BrowsableObject):
506 """a class that will be used into the python code, mainly for usability purposes"""
507 def _sum(self, code, from_date, to_date=None):
509 to_date = datetime.now().strftime('%Y-%m-%d')
511 self.cr.execute("SELECT sum(number_of_days) as number_of_days, sum(number_of_hours) as number_of_hours\
512 FROM hr_payslip as hp, hr_payslip_worked_days as pi \
513 WHERE hp.employee_id = %s AND hp.state = 'done'\
514 AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s",
515 (self.employee_id, from_date, to_date, code))
516 return self.cr.fetchone()
518 def sum(self, code, from_date, to_date=None):
519 res = self._sum(code, from_date, to_date)
520 return res and res[0] or 0.0
522 def sum_hours(self, code, from_date, to_date=None):
523 res = self._sum(code, from_date, to_date)
524 return res and res[1] or 0.0
526 class Payslips(BrowsableObject):
527 """a class that will be used into the python code, mainly for usability purposes"""
529 def sum(self, code, from_date, to_date=None):
531 to_date = datetime.now().strftime('%Y-%m-%d')
532 self.cr.execute("SELECT sum(case when hp.credit_note = False then (pl.total) else (-pl.total) end)\
533 FROM hr_payslip as hp, hr_payslip_line as pl \
534 WHERE hp.employee_id = %s AND hp.state = 'done' \
535 AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id AND pl.code = %s",
536 (self.employee_id, from_date, to_date, code))
537 res = self.cr.fetchone()
538 return res and res[0] or 0.0
540 #we keep a dict with the result because a value can be overwritten by another rule with the same code
545 payslip_obj = self.pool.get('hr.payslip')
546 inputs_obj = self.pool.get('hr.payslip.worked_days')
547 obj_rule = self.pool.get('hr.salary.rule')
548 payslip = payslip_obj.browse(cr, uid, payslip_id, context=context)
550 for worked_days_line in payslip.worked_days_line_ids:
551 worked_days[worked_days_line.code] = worked_days_line
553 for input_line in payslip.input_line_ids:
554 inputs[input_line.code] = input_line
556 categories_obj = BrowsableObject(self.pool, cr, uid, payslip.employee_id.id, categories_dict)
557 input_obj = InputLine(self.pool, cr, uid, payslip.employee_id.id, inputs)
558 worked_days_obj = WorkedDays(self.pool, cr, uid, payslip.employee_id.id, worked_days)
559 payslip_obj = Payslips(self.pool, cr, uid, payslip.employee_id.id, payslip)
560 rules_obj = BrowsableObject(self.pool, cr, uid, payslip.employee_id.id, rules)
562 localdict = {'categories': categories_obj, 'rules': rules_obj, 'payslip': payslip_obj, 'worked_days': worked_days_obj, 'inputs': input_obj}
563 #get the ids of the structures on the contracts and their parent id as well
564 structure_ids = self.pool.get('hr.contract').get_all_structures(cr, uid, contract_ids, context=context)
565 #get the rules of the structure and thier children
566 rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
567 #run the rules by sequence
568 sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
570 for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
571 employee = contract.employee_id
572 localdict.update({'employee': employee, 'contract': contract})
573 for rule in obj_rule.browse(cr, uid, sorted_rule_ids, context=context):
574 key = rule.code + '-' + str(contract.id)
575 localdict['result'] = None
576 localdict['result_qty'] = 1.0
577 #check if the rule can be applied
578 if obj_rule.satisfy_condition(cr, uid, rule.id, localdict, context=context) and rule.id not in blacklist:
579 #compute the amount of the rule
580 amount, qty = obj_rule.compute_rule(cr, uid, rule.id, localdict, context=context)
581 #check if there is already a rule computed with that code
582 previous_amount = rule.code in localdict and localdict[rule.code] or 0.0
583 #set/overwrite the amount computed for this rule in the localdict
584 localdict[rule.code] = amount * qty
585 rules[rule.code] = rule
586 #sum the amount for its salary category
587 localdict = _sum_salary_rule_category(localdict, rule.category_id, (amount * qty) - previous_amount)
588 #create/overwrite the rule in the temporary results
590 'salary_rule_id': rule.id,
591 'contract_id': contract.id,
594 'category_id': rule.category_id.id,
595 'sequence': rule.sequence,
596 'appears_on_payslip': rule.appears_on_payslip,
597 'condition_select': rule.condition_select,
598 'condition_python': rule.condition_python,
599 'condition_range': rule.condition_range,
600 'condition_range_min': rule.condition_range_min,
601 'condition_range_max': rule.condition_range_max,
602 'amount_select': rule.amount_select,
603 'amount_fix': rule.amount_fix,
604 'amount_python_compute': rule.amount_python_compute,
605 'amount_percentage': rule.amount_percentage,
606 'amount_percentage_base': rule.amount_percentage_base,
607 'register_id': rule.register_id.id,
609 'employee_id': contract.employee_id.id,
613 #blacklist this rule and its children
614 blacklist += [id for id, seq in self.pool.get('hr.salary.rule')._recursive_search_of_rules(cr, uid, [rule], context=context)]
616 result = [value for code, value in result_dict.items()]
619 def onchange_employee_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
620 empolyee_obj = self.pool.get('hr.employee')
621 contract_obj = self.pool.get('hr.contract')
622 worked_days_obj = self.pool.get('hr.payslip.worked_days')
623 input_obj = self.pool.get('hr.payslip.input')
627 #delete old worked days lines
628 old_worked_days_ids = ids and worked_days_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
629 if old_worked_days_ids:
630 worked_days_obj.unlink(cr, uid, old_worked_days_ids, context=context)
632 #delete old input lines
633 old_input_ids = ids and input_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
635 input_obj.unlink(cr, uid, old_input_ids, context=context)
641 'input_line_ids': [],
642 'worked_days_line_ids': [],
643 #'details_by_salary_head':[], TODO put me back
645 'contract_id': False,
651 ttyme = datetime.fromtimestamp(time.mktime(time.strptime(date_from, "%Y-%m-%d")))
652 employee_id = empolyee_obj.browse(cr, uid, employee_id, context=context)
653 res['value'].update({
654 'name': _('Salary Slip of %s for %s') % (employee_id.name, tools.ustr(ttyme.strftime('%B-%Y'))),
655 'company_id': employee_id.company_id.id
658 if not context.get('contract', False):
659 #fill with the first contract of the employee
660 contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
661 res['value'].update({
662 'struct_id': contract_ids and contract_obj.read(cr, uid, contract_ids[0], ['struct_id'], context=context)['struct_id'][0] or False,
663 'contract_id': contract_ids and contract_ids[0] or False,
667 #set the list of contract for which the input have to be filled
668 contract_ids = [contract_id]
669 #fill the structure with the one on the selected contract
670 contract_record = contract_obj.browse(cr, uid, contract_id, context=context)
671 res['value'].update({
672 'struct_id': contract_record.struct_id.id,
673 'contract_id': contract_id
676 #if we don't give the contract, then the input to fill should be for all current contracts of the employee
677 contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
681 #computation of the salary input
682 worked_days_line_ids = self.get_worked_day_lines(cr, uid, contract_ids, date_from, date_to, context=context)
683 input_line_ids = self.get_inputs(cr, uid, contract_ids, date_from, date_to, context=context)
684 res['value'].update({
685 'worked_days_line_ids': worked_days_line_ids,
686 'input_line_ids': input_line_ids,
690 def onchange_contract_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
691 #TODO it seems to be the mess in the onchanges, we should have onchange_employee => onchange_contract => doing all the things
699 context.update({'contract': True})
701 res['value'].update({'struct_id': False})
702 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)
706 class hr_payslip_worked_days(osv.osv):
711 _name = 'hr.payslip.worked_days'
712 _description = 'Payslip Worked Days'
714 'name': fields.char('Description', size=256, required=True),
715 'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True),
716 'sequence': fields.integer('Sequence', required=True,),
717 'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
718 'number_of_days': fields.float('Number of Days'),
719 'number_of_hours': fields.float('Number of Hours'),
720 'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
722 _order = 'payslip_id, sequence'
726 hr_payslip_worked_days()
728 class hr_payslip_input(osv.osv):
733 _name = 'hr.payslip.input'
734 _description = 'Payslip Input'
736 'name': fields.char('Description', size=256, required=True),
737 'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True),
738 'sequence': fields.integer('Sequence', required=True,),
739 'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
740 '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."),
741 'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
743 _order = 'payslip_id, sequence'
751 class hr_salary_rule(osv.osv):
753 _name = 'hr.salary.rule'
755 'name':fields.char('Name', size=256, required=True, readonly=False),
756 '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."),
757 'sequence': fields.integer('Sequence', required=True, help='Use to arrange calculation sequence'),
758 '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."),
759 'category_id':fields.many2one('hr.salary.rule.category', 'Category', required=True),
760 '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."),
761 'appears_on_payslip': fields.boolean('Appears on Payslip', help="Used for the display of rule on payslip"),
762 'parent_rule_id':fields.many2one('hr.salary.rule', 'Parent Salary Rule', select=True),
763 'company_id':fields.many2one('res.company', 'Company', required=False),
764 'condition_select': fields.selection([('none', 'Always True'),('range', 'Range'), ('python', 'Python Expression')], "Condition Based on", required=True),
765 '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 categories code field in small letter as a variable name i.e. hra, ma, lta, etc...., also you can use, static varible basic'),
766 '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
767 'condition_range_min': fields.float('Minimum Range', required=False, help="The minimum amount, applied for this rule."),
768 'condition_range_max': fields.float('Maximum Range', required=False, help="The maximum amount, applied for this rule."),
769 'amount_select':fields.selection([
770 ('percentage','Percentage (%)'),
771 ('fix','Fixed Amount'),
772 ('code','Python Code'),
773 ],'Amount Type', select=True, required=True, help="The computation method for the rule amount."),
774 'amount_fix': fields.float('Fixed Amount', digits_compute=dp.get_precision('Payroll'),),
775 'amount_percentage': fields.float('Percentage (%)', digits_compute=dp.get_precision('Payroll'), help='For example, enter 50.0 to apply a percentage of 50%'),
776 'amount_python_compute':fields.text('Python Code'),
777 'amount_percentage_base':fields.char('Percentage based on',size=1024, required=False, readonly=False, help='result will be affected to a variable'),
778 'child_ids':fields.one2many('hr.salary.rule', 'parent_rule_id', 'Child Salary Rule'),
779 'register_id':fields.many2one('hr.contribution.register', 'Contribution Register', help="Eventual third party involved in the salary payment of the employees."),
780 'input_ids': fields.one2many('hr.rule.input', 'input_id', 'Inputs'),
781 'note':fields.text('Description'),
784 'amount_python_compute': '''
785 # Available variables:
786 #----------------------
787 # payslip: object containing the payslips
788 # employee: hr.employee object
789 # contract: hr.contract object
790 # rules: object containing the rules code (previously computed)
791 # categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category).
792 # worked_days: object containing the computed worked days.
793 # inputs: object containing the computed inputs.
795 # Note: returned value have to be set in the variable 'result'
797 result = contract.wage * 0.10''',
800 # Available variables:
801 #----------------------
802 # payslip: object containing the payslips
803 # employee: hr.employee object
804 # contract: hr.contract object
805 # rules: object containing the rules code (previously computed)
806 # categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category).
807 # worked_days: object containing the computed worked days
808 # inputs: object containing the computed inputs
810 # Note: returned value have to be set in the variable 'result'
812 result = rules.NET > categories.NET * 0.10''',
813 'condition_range': 'contract.wage',
815 'appears_on_payslip': True,
817 'company_id': lambda self, cr, uid, context: \
818 self.pool.get('res.users').browse(cr, uid, uid,
819 context=context).company_id.id,
820 'condition_select': 'none',
821 'amount_select': 'fix',
823 'amount_percentage': 0.0,
827 def _recursive_search_of_rules(self, cr, uid, rule_ids, context=None):
829 @param rule_ids: list of browse record
830 @return: returns a list of tuple (id, sequence) which are all the children of the passed rule_ids
833 for rule in rule_ids:
835 children_rules += self._recursive_search_of_rules(cr, uid, rule.child_ids, context=context)
836 return [(r.id, r.sequence) for r in rule_ids] + children_rules
838 #TODO should add some checks on the type of result (should be float)
839 def compute_rule(self, cr, uid, rule_id, localdict, context=None):
841 @param rule_id: id of rule to compute
842 @param localdict: dictionary containing the environement in which to compute the rule
843 @return: returns the result of computation and the quantity as floats
845 rule = self.browse(cr, uid, rule_id, context=context)
846 if rule.amount_select == 'fix':
848 return rule.amount_fix, eval(rule.quantity, localdict)
850 raise osv.except_osv(_('Error'), _('Wrong quantity defined for salary rule %s (%s)')% (rule.name, rule.code))
851 elif rule.amount_select == 'percentage':
853 amount = rule.amount_percentage * eval(rule.amount_percentage_base, localdict) / 100
854 return amount, eval(rule.quantity, localdict)
856 raise osv.except_osv(_('Error'), _('Wrong percentage base or quantity defined for salary rule %s (%s)')% (rule.name, rule.code))
859 eval(rule.amount_python_compute, localdict, mode='exec', nocopy=True)
860 return localdict['result'], 'result_qty' in localdict and localdict['result_qty'] or 1.0
862 raise osv.except_osv(_('Error'), _('Wrong python code defined for salary rule %s (%s) ')% (rule.name, rule.code))
864 def satisfy_condition(self, cr, uid, rule_id, localdict, context=None):
866 @param rule_id: id of hr.salary.rule to be tested
867 @param contract_id: id of hr.contract to be tested
868 @return: returns True if the given rule match the condition for the given contract. Return False otherwise.
870 rule = self.browse(cr, uid, rule_id, context=context)
872 if rule.condition_select == 'none':
874 elif rule.condition_select == 'range':
876 result = eval(rule.condition_range, localdict)
877 return rule.condition_range_min <= result and result <= rule.condition_range_max or False
879 raise osv.except_osv(_('Error'), _('Wrong range condition defined for salary rule %s (%s)')% (rule.name, rule.code))
882 eval(rule.condition_python, localdict, mode='exec', nocopy=True)
883 return 'result' in localdict and localdict['result'] or False
885 raise osv.except_osv(_('Error'), _('Wrong python condition defined for salary rule %s (%s)')% (rule.name, rule.code))
889 class hr_rule_input(osv.osv):
894 _name = 'hr.rule.input'
895 _description = 'Salary Rule Input'
897 'name': fields.char('Description', size=256, required=True),
898 'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
899 'input_id': fields.many2one('hr.salary.rule', 'Salary Rule Input', required=True)
904 class hr_payslip_line(osv.osv):
909 _name = 'hr.payslip.line'
910 _inherit = 'hr.salary.rule'
911 _description = 'Payslip Line'
912 _order = 'contract_id, sequence'
914 def _calculate_total(self, cr, uid, ids, name, args, context):
915 if not ids: return {}
917 for line in self.browse(cr, uid, ids, context=context):
918 res[line.id] = float(line.quantity) * line.amount
922 'slip_id':fields.many2one('hr.payslip', 'Pay Slip', required=True),
923 'salary_rule_id':fields.many2one('hr.salary.rule', 'Rule', required=True),
924 'employee_id':fields.many2one('hr.employee', 'Employee', required=True),
925 'contract_id':fields.many2one('hr.contract', 'Contract', required=True),
926 'amount': fields.float('Amount', digits_compute=dp.get_precision('Payroll')),
927 'quantity': fields.float('Quantity', digits_compute=dp.get_precision('Payroll')),
928 'total': fields.function(_calculate_total, method=True, type='float', string='Total', digits_compute=dp.get_precision('Payroll'),store=True ),
933 class hr_payroll_structure(osv.osv):
935 _inherit = 'hr.payroll.structure'
937 'rule_ids':fields.many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id', 'Salary Rules'),
940 hr_payroll_structure()
942 class hr_employee(osv.osv):
947 _inherit = 'hr.employee'
948 _description = 'Employee'
950 def _calculate_total_wage(self, cr, uid, ids, name, args, context):
951 if not ids: return {}
953 current_date = datetime.now().strftime('%Y-%m-%d')
954 for employee in self.browse(cr, uid, ids, context=context):
955 if not employee.contract_ids:
956 res[employee.id] = {'basic': 0.0}
958 cr.execute( 'SELECT SUM(wage) '\
960 'WHERE employee_id = %s '\
961 'AND date_start <= %s '\
962 'AND (date_end > %s OR date_end is NULL)',
963 (employee.id, current_date, current_date))
964 result = dict(cr.dictfetchone())
965 res[employee.id] = {'basic': result['sum']}
969 'slip_ids':fields.one2many('hr.payslip', 'employee_id', 'Payslips', required=False, readonly=True),
970 '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."),
975 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: