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 Batches'
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 ], 'Status', 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 ], 'Status', 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 Batches', 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 _check_dates(self, cr, uid, ids, context=None):
302 for payslip in self.browse(cr, uid, ids, context=context):
303 if payslip.date_from > payslip.date_to:
307 _constraints = [(_check_dates, "Payslip 'Date From' must be before 'Date To'.", ['date_from', 'date_to'])]
309 def copy(self, cr, uid, id, default=None, context=None):
312 company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
317 'company_id': company_id,
319 'basic_before_leaves': 0.0,
322 return super(hr_payslip, self).copy(cr, uid, id, default, context=context)
324 def cancel_sheet(self, cr, uid, ids, context=None):
325 return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
327 def process_sheet(self, cr, uid, ids, context=None):
328 return self.write(cr, uid, ids, {'paid': True, 'state': 'done'}, context=context)
330 def hr_verify_sheet(self, cr, uid, ids, context=None):
331 return self.write(cr, uid, ids, {'state': 'verify'}, context=context)
333 def refund_sheet(self, cr, uid, ids, context=None):
334 mod_obj = self.pool.get('ir.model.data')
335 wf_service = netsvc.LocalService("workflow")
336 for payslip in self.browse(cr, uid, ids, context=context):
337 id_copy = self.copy(cr, uid, payslip.id, {'credit_note': True, 'name': _('Refund: ')+payslip.name}, context=context)
338 self.compute_sheet(cr, uid, [id_copy], context=context)
339 wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'hr_verify_sheet', cr)
340 wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'process_sheet', cr)
342 form_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_form')
343 form_res = form_id and form_id[1] or False
344 tree_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_tree')
345 tree_res = tree_id and tree_id[1] or False
347 'name':_("Refund Payslip"),
348 'view_mode': 'tree, form',
351 'res_model': 'hr.payslip',
352 'type': 'ir.actions.act_window',
355 'domain': "[('id', 'in', %s)]" % [id_copy],
356 'views': [(tree_res, 'tree'), (form_res, 'form')],
360 def check_done(self, cr, uid, ids, context=None):
363 #TODO move this function into hr_contract module, on hr.employee object
364 def get_contract(self, cr, uid, employee, date_from, date_to, context=None):
366 @param employee: browse record of employee
367 @param date_from: date field
368 @param date_to: date field
369 @return: returns the ids of all the contracts for the given employee that need to be considered for the given dates
371 contract_obj = self.pool.get('hr.contract')
373 #a contract is valid if it ends between the given dates
374 clause_1 = ['&',('date_end', '<=', date_to),('date_end','>=', date_from)]
375 #OR if it starts between the given dates
376 clause_2 = ['&',('date_start', '<=', date_to),('date_start','>=', date_from)]
377 #OR if it starts before the date_from and finish after the date_end (or never finish)
378 clause_3 = [('date_start','<=', date_from),'|',('date_end', '=', False),('date_end','>=', date_to)]
379 clause_final = [('employee_id', '=', employee.id),'|','|'] + clause_1 + clause_2 + clause_3
380 contract_ids = contract_obj.search(cr, uid, clause_final, context=context)
383 def compute_sheet(self, cr, uid, ids, context=None):
384 slip_line_pool = self.pool.get('hr.payslip.line')
385 sequence_obj = self.pool.get('ir.sequence')
386 for payslip in self.browse(cr, uid, ids, context=context):
387 number = payslip.number or sequence_obj.get(cr, uid, 'salary.slip')
388 #delete old payslip lines
389 old_slipline_ids = slip_line_pool.search(cr, uid, [('slip_id', '=', payslip.id)], context=context)
392 slip_line_pool.unlink(cr, uid, old_slipline_ids, context=context)
393 if payslip.contract_id:
394 #set the list of contract for which the rules have to be applied
395 contract_ids = [payslip.contract_id.id]
397 #if we don't give the contract, then the rules to apply should be for all current contracts of the employee
398 contract_ids = self.get_contract(cr, uid, payslip.employee_id, payslip.date_from, payslip.date_to, context=context)
399 lines = [(0,0,line) for line in self.pool.get('hr.payslip').get_payslip_lines(cr, uid, contract_ids, payslip.id, context=context)]
400 self.write(cr, uid, [payslip.id], {'line_ids': lines, 'number': number,}, context=context)
403 def get_worked_day_lines(self, cr, uid, contract_ids, date_from, date_to, context=None):
405 @param contract_ids: list of contract id
406 @return: returns a list of dict containing the input that should be applied for the given contract between date_from and date_to
408 def was_on_leave(employee_id, datetime_day, context=None):
410 day = datetime_day.strftime("%Y-%m-%d")
411 holiday_ids = self.pool.get('hr.holidays').search(cr, uid, [('state','=','validate'),('employee_id','=',employee_id),('type','=','remove'),('date_from','<=',day),('date_to','>=',day)])
413 res = self.pool.get('hr.holidays').browse(cr, uid, holiday_ids, context=context)[0].holiday_status_id.name
417 for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
418 if not contract.working_hours:
419 #fill only if the contract as a working schedule linked
422 'name': _("Normal Working Days paid at 100%"),
425 'number_of_days': 0.0,
426 'number_of_hours': 0.0,
427 'contract_id': contract.id,
430 day_from = datetime.strptime(date_from,"%Y-%m-%d")
431 day_to = datetime.strptime(date_to,"%Y-%m-%d")
432 nb_of_days = (day_to - day_from).days + 1
433 for day in range(0, nb_of_days):
434 working_hours_on_day = self.pool.get('resource.calendar').working_hours_on_day(cr, uid, contract.working_hours, day_from + timedelta(days=day), context)
435 if working_hours_on_day:
436 #the employee had to work
437 leave_type = was_on_leave(contract.employee_id.id, day_from + timedelta(days=day), context=context)
439 #if he was on leave, fill the leaves dict
440 if leave_type in leaves:
441 leaves[leave_type]['number_of_days'] += 1.0
442 leaves[leave_type]['number_of_hours'] += working_hours_on_day
444 leaves[leave_type] = {
448 'number_of_days': 1.0,
449 'number_of_hours': working_hours_on_day,
450 'contract_id': contract.id,
453 #add the input vals to tmp (increment if existing)
454 attendances['number_of_days'] += 1.0
455 attendances['number_of_hours'] += working_hours_on_day
456 leaves = [value for key,value in leaves.items()]
457 res += [attendances] + leaves
460 def get_inputs(self, cr, uid, contract_ids, date_from, date_to, context=None):
462 contract_obj = self.pool.get('hr.contract')
463 rule_obj = self.pool.get('hr.salary.rule')
465 structure_ids = contract_obj.get_all_structures(cr, uid, contract_ids, context=context)
466 rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
467 sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
469 for contract in contract_obj.browse(cr, uid, contract_ids, context=context):
470 for rule in rule_obj.browse(cr, uid, sorted_rule_ids, context=context):
472 for input in rule.input_ids:
476 'contract_id': contract.id,
481 def get_payslip_lines(self, cr, uid, contract_ids, payslip_id, context):
482 def _sum_salary_rule_category(localdict, category, amount):
483 if category.parent_id:
484 localdict = _sum_salary_rule_category(localdict, category.parent_id, amount)
485 localdict['categories'].dict[category.code] = category.code in localdict['categories'].dict and localdict['categories'].dict[category.code] + amount or amount
488 class BrowsableObject(object):
489 def __init__(self, pool, cr, uid, employee_id, dict):
493 self.employee_id = employee_id
496 def __getattr__(self, attr):
497 return attr in self.dict and self.dict.__getitem__(attr) or 0.0
499 class InputLine(BrowsableObject):
500 """a class that will be used into the python code, mainly for usability purposes"""
501 def sum(self, code, from_date, to_date=None):
503 to_date = datetime.now().strftime('%Y-%m-%d')
505 self.cr.execute("SELECT sum(amount) as sum\
506 FROM hr_payslip as hp, hr_payslip_input as pi \
507 WHERE hp.employee_id = %s AND hp.state = 'done' \
508 AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s",
509 (self.employee_id, from_date, to_date, code))
510 res = self.cr.fetchone()[0]
513 class WorkedDays(BrowsableObject):
514 """a class that will be used into the python code, mainly for usability purposes"""
515 def _sum(self, code, from_date, to_date=None):
517 to_date = datetime.now().strftime('%Y-%m-%d')
519 self.cr.execute("SELECT sum(number_of_days) as number_of_days, sum(number_of_hours) as number_of_hours\
520 FROM hr_payslip as hp, hr_payslip_worked_days as pi \
521 WHERE hp.employee_id = %s AND hp.state = 'done'\
522 AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s",
523 (self.employee_id, from_date, to_date, code))
524 return self.cr.fetchone()
526 def sum(self, code, from_date, to_date=None):
527 res = self._sum(code, from_date, to_date)
528 return res and res[0] or 0.0
530 def sum_hours(self, code, from_date, to_date=None):
531 res = self._sum(code, from_date, to_date)
532 return res and res[1] or 0.0
534 class Payslips(BrowsableObject):
535 """a class that will be used into the python code, mainly for usability purposes"""
537 def sum(self, code, from_date, to_date=None):
539 to_date = datetime.now().strftime('%Y-%m-%d')
540 self.cr.execute("SELECT sum(case when hp.credit_note = False then (pl.total) else (-pl.total) end)\
541 FROM hr_payslip as hp, hr_payslip_line as pl \
542 WHERE hp.employee_id = %s AND hp.state = 'done' \
543 AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id AND pl.code = %s",
544 (self.employee_id, from_date, to_date, code))
545 res = self.cr.fetchone()
546 return res and res[0] or 0.0
548 #we keep a dict with the result because a value can be overwritten by another rule with the same code
553 payslip_obj = self.pool.get('hr.payslip')
554 inputs_obj = self.pool.get('hr.payslip.worked_days')
555 obj_rule = self.pool.get('hr.salary.rule')
556 payslip = payslip_obj.browse(cr, uid, payslip_id, context=context)
558 for worked_days_line in payslip.worked_days_line_ids:
559 worked_days[worked_days_line.code] = worked_days_line
561 for input_line in payslip.input_line_ids:
562 inputs[input_line.code] = input_line
564 categories_obj = BrowsableObject(self.pool, cr, uid, payslip.employee_id.id, categories_dict)
565 input_obj = InputLine(self.pool, cr, uid, payslip.employee_id.id, inputs)
566 worked_days_obj = WorkedDays(self.pool, cr, uid, payslip.employee_id.id, worked_days)
567 payslip_obj = Payslips(self.pool, cr, uid, payslip.employee_id.id, payslip)
568 rules_obj = BrowsableObject(self.pool, cr, uid, payslip.employee_id.id, rules)
570 localdict = {'categories': categories_obj, 'rules': rules_obj, 'payslip': payslip_obj, 'worked_days': worked_days_obj, 'inputs': input_obj}
571 #get the ids of the structures on the contracts and their parent id as well
572 structure_ids = self.pool.get('hr.contract').get_all_structures(cr, uid, contract_ids, context=context)
573 #get the rules of the structure and thier children
574 rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
575 #run the rules by sequence
576 sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
578 for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
579 employee = contract.employee_id
580 localdict.update({'employee': employee, 'contract': contract})
581 for rule in obj_rule.browse(cr, uid, sorted_rule_ids, context=context):
582 key = rule.code + '-' + str(contract.id)
583 localdict['result'] = None
584 localdict['result_qty'] = 1.0
585 #check if the rule can be applied
586 if obj_rule.satisfy_condition(cr, uid, rule.id, localdict, context=context) and rule.id not in blacklist:
587 #compute the amount of the rule
588 amount, qty, rate = obj_rule.compute_rule(cr, uid, rule.id, localdict, context=context)
589 #check if there is already a rule computed with that code
590 previous_amount = rule.code in localdict and localdict[rule.code] or 0.0
591 #set/overwrite the amount computed for this rule in the localdict
592 tot_rule = amount * qty * rate / 100.0
593 localdict[rule.code] = tot_rule
594 rules[rule.code] = rule
595 #sum the amount for its salary category
596 localdict = _sum_salary_rule_category(localdict, rule.category_id, tot_rule - previous_amount)
597 #create/overwrite the rule in the temporary results
599 'salary_rule_id': rule.id,
600 'contract_id': contract.id,
603 'category_id': rule.category_id.id,
604 'sequence': rule.sequence,
605 'appears_on_payslip': rule.appears_on_payslip,
606 'condition_select': rule.condition_select,
607 'condition_python': rule.condition_python,
608 'condition_range': rule.condition_range,
609 'condition_range_min': rule.condition_range_min,
610 'condition_range_max': rule.condition_range_max,
611 'amount_select': rule.amount_select,
612 'amount_fix': rule.amount_fix,
613 'amount_python_compute': rule.amount_python_compute,
614 'amount_percentage': rule.amount_percentage,
615 'amount_percentage_base': rule.amount_percentage_base,
616 'register_id': rule.register_id.id,
618 'employee_id': contract.employee_id.id,
623 #blacklist this rule and its children
624 blacklist += [id for id, seq in self.pool.get('hr.salary.rule')._recursive_search_of_rules(cr, uid, [rule], context=context)]
626 result = [value for code, value in result_dict.items()]
629 def onchange_employee_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
630 empolyee_obj = self.pool.get('hr.employee')
631 contract_obj = self.pool.get('hr.contract')
632 worked_days_obj = self.pool.get('hr.payslip.worked_days')
633 input_obj = self.pool.get('hr.payslip.input')
637 #delete old worked days lines
638 old_worked_days_ids = ids and worked_days_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
639 if old_worked_days_ids:
640 worked_days_obj.unlink(cr, uid, old_worked_days_ids, context=context)
642 #delete old input lines
643 old_input_ids = ids and input_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
645 input_obj.unlink(cr, uid, old_input_ids, context=context)
651 'input_line_ids': [],
652 'worked_days_line_ids': [],
653 #'details_by_salary_head':[], TODO put me back
655 'contract_id': False,
659 if (not employee_id) or (not date_from) or (not date_to):
661 ttyme = datetime.fromtimestamp(time.mktime(time.strptime(date_from, "%Y-%m-%d")))
662 employee_id = empolyee_obj.browse(cr, uid, employee_id, context=context)
663 res['value'].update({
664 'name': _('Salary Slip of %s for %s') % (employee_id.name, tools.ustr(ttyme.strftime('%B-%Y'))),
665 'company_id': employee_id.company_id.id
668 if not context.get('contract', False):
669 #fill with the first contract of the employee
670 contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
671 res['value'].update({
672 'struct_id': contract_ids and contract_obj.read(cr, uid, contract_ids[0], ['struct_id'], context=context)['struct_id'][0] or False,
673 'contract_id': contract_ids and contract_ids[0] or False,
677 #set the list of contract for which the input have to be filled
678 contract_ids = [contract_id]
679 #fill the structure with the one on the selected contract
680 contract_record = contract_obj.browse(cr, uid, contract_id, context=context)
681 res['value'].update({
682 'struct_id': contract_record.struct_id.id,
683 'contract_id': contract_id
686 #if we don't give the contract, then the input to fill should be for all current contracts of the employee
687 contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
691 #computation of the salary input
692 worked_days_line_ids = self.get_worked_day_lines(cr, uid, contract_ids, date_from, date_to, context=context)
693 input_line_ids = self.get_inputs(cr, uid, contract_ids, date_from, date_to, context=context)
694 res['value'].update({
695 'worked_days_line_ids': worked_days_line_ids,
696 'input_line_ids': input_line_ids,
700 def onchange_contract_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
701 #TODO it seems to be the mess in the onchanges, we should have onchange_employee => onchange_contract => doing all the things
709 context.update({'contract': True})
711 res['value'].update({'struct_id': False})
712 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)
716 class hr_payslip_worked_days(osv.osv):
721 _name = 'hr.payslip.worked_days'
722 _description = 'Payslip Worked Days'
724 'name': fields.char('Description', size=256, required=True),
725 'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade', select=True),
726 'sequence': fields.integer('Sequence', required=True, select=True),
727 'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
728 'number_of_days': fields.float('Number of Days'),
729 'number_of_hours': fields.float('Number of Hours'),
730 'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
732 _order = 'payslip_id, sequence'
736 hr_payslip_worked_days()
738 class hr_payslip_input(osv.osv):
743 _name = 'hr.payslip.input'
744 _description = 'Payslip Input'
746 'name': fields.char('Description', size=256, required=True),
747 'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade', select=True),
748 'sequence': fields.integer('Sequence', required=True, select=True),
749 'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
750 '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."),
751 'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
753 _order = 'payslip_id, sequence'
761 class hr_salary_rule(osv.osv):
763 _name = 'hr.salary.rule'
765 'name':fields.char('Name', size=256, required=True, readonly=False),
766 '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."),
767 'sequence': fields.integer('Sequence', required=True, help='Use to arrange calculation sequence', select=True),
768 '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."),
769 'category_id':fields.many2one('hr.salary.rule.category', 'Category', required=True),
770 '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."),
771 'appears_on_payslip': fields.boolean('Appears on Payslip', help="Used for the display of rule on payslip"),
772 'parent_rule_id':fields.many2one('hr.salary.rule', 'Parent Salary Rule', select=True),
773 'company_id':fields.many2one('res.company', 'Company', required=False),
774 'condition_select': fields.selection([('none', 'Always True'),('range', 'Range'), ('python', 'Python Expression')], "Condition Based on", required=True),
775 'condition_range':fields.char('Range Based on',size=1024, readonly=False, help='This will be used to compute the % fields values; in general it is on basic, but you can also use categories code fields in lowercase as a variable names (hra, ma, lta, etc.) and the variable basic.'),
776 '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
777 'condition_range_min': fields.float('Minimum Range', required=False, help="The minimum amount, applied for this rule."),
778 'condition_range_max': fields.float('Maximum Range', required=False, help="The maximum amount, applied for this rule."),
779 'amount_select':fields.selection([
780 ('percentage','Percentage (%)'),
781 ('fix','Fixed Amount'),
782 ('code','Python Code'),
783 ],'Amount Type', select=True, required=True, help="The computation method for the rule amount."),
784 'amount_fix': fields.float('Fixed Amount', digits_compute=dp.get_precision('Payroll'),),
785 'amount_percentage': fields.float('Percentage (%)', digits_compute=dp.get_precision('Payroll Rate'), help='For example, enter 50.0 to apply a percentage of 50%'),
786 'amount_python_compute':fields.text('Python Code'),
787 'amount_percentage_base':fields.char('Percentage based on',size=1024, required=False, readonly=False, help='result will be affected to a variable'),
788 'child_ids':fields.one2many('hr.salary.rule', 'parent_rule_id', 'Child Salary Rule'),
789 'register_id':fields.many2one('hr.contribution.register', 'Contribution Register', help="Eventual third party involved in the salary payment of the employees."),
790 'input_ids': fields.one2many('hr.rule.input', 'input_id', 'Inputs'),
791 'note':fields.text('Description'),
794 'amount_python_compute': '''
795 # Available variables:
796 #----------------------
797 # payslip: object containing the payslips
798 # employee: hr.employee object
799 # contract: hr.contract object
800 # rules: object containing the rules code (previously computed)
801 # categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category).
802 # worked_days: object containing the computed worked days.
803 # inputs: object containing the computed inputs.
805 # Note: returned value have to be set in the variable 'result'
807 result = contract.wage * 0.10''',
810 # Available variables:
811 #----------------------
812 # payslip: object containing the payslips
813 # employee: hr.employee object
814 # contract: hr.contract object
815 # rules: object containing the rules code (previously computed)
816 # categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category).
817 # worked_days: object containing the computed worked days
818 # inputs: object containing the computed inputs
820 # Note: returned value have to be set in the variable 'result'
822 result = rules.NET > categories.NET * 0.10''',
823 'condition_range': 'contract.wage',
825 'appears_on_payslip': True,
827 'company_id': lambda self, cr, uid, context: \
828 self.pool.get('res.users').browse(cr, uid, uid,
829 context=context).company_id.id,
830 'condition_select': 'none',
831 'amount_select': 'fix',
833 'amount_percentage': 0.0,
837 def _recursive_search_of_rules(self, cr, uid, rule_ids, context=None):
839 @param rule_ids: list of browse record
840 @return: returns a list of tuple (id, sequence) which are all the children of the passed rule_ids
843 for rule in rule_ids:
845 children_rules += self._recursive_search_of_rules(cr, uid, rule.child_ids, context=context)
846 return [(r.id, r.sequence) for r in rule_ids] + children_rules
848 #TODO should add some checks on the type of result (should be float)
849 def compute_rule(self, cr, uid, rule_id, localdict, context=None):
851 :param rule_id: id of rule to compute
852 :param localdict: dictionary containing the environement in which to compute the rule
853 :return: returns a tuple build as the base/amount computed, the quantity and the rate
854 :rtype: (float, float, float)
856 rule = self.browse(cr, uid, rule_id, context=context)
857 if rule.amount_select == 'fix':
859 return rule.amount_fix, eval(rule.quantity, localdict), 100.0
861 raise osv.except_osv(_('Error'), _('Wrong quantity defined for salary rule %s (%s)')% (rule.name, rule.code))
862 elif rule.amount_select == 'percentage':
864 return eval(rule.amount_percentage_base, localdict), eval(rule.quantity, localdict), rule.amount_percentage
866 raise osv.except_osv(_('Error'), _('Wrong percentage base or quantity defined for salary rule %s (%s)')% (rule.name, rule.code))
869 eval(rule.amount_python_compute, localdict, mode='exec', nocopy=True)
870 return localdict['result'], 'result_qty' in localdict and localdict['result_qty'] or 1.0, 'result_rate' in localdict and localdict['result_rate'] or 100.0
872 raise osv.except_osv(_('Error'), _('Wrong python code defined for salary rule %s (%s) ')% (rule.name, rule.code))
874 def satisfy_condition(self, cr, uid, rule_id, localdict, context=None):
876 @param rule_id: id of hr.salary.rule to be tested
877 @param contract_id: id of hr.contract to be tested
878 @return: returns True if the given rule match the condition for the given contract. Return False otherwise.
880 rule = self.browse(cr, uid, rule_id, context=context)
882 if rule.condition_select == 'none':
884 elif rule.condition_select == 'range':
886 result = eval(rule.condition_range, localdict)
887 return rule.condition_range_min <= result and result <= rule.condition_range_max or False
889 raise osv.except_osv(_('Error'), _('Wrong range condition defined for salary rule %s (%s)')% (rule.name, rule.code))
892 eval(rule.condition_python, localdict, mode='exec', nocopy=True)
893 return 'result' in localdict and localdict['result'] or False
895 raise osv.except_osv(_('Error'), _('Wrong python condition defined for salary rule %s (%s)')% (rule.name, rule.code))
899 class hr_rule_input(osv.osv):
904 _name = 'hr.rule.input'
905 _description = 'Salary Rule Input'
907 'name': fields.char('Description', size=256, required=True),
908 'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
909 'input_id': fields.many2one('hr.salary.rule', 'Salary Rule Input', required=True)
914 class hr_payslip_line(osv.osv):
919 _name = 'hr.payslip.line'
920 _inherit = 'hr.salary.rule'
921 _description = 'Payslip Line'
922 _order = 'contract_id, sequence'
924 def _calculate_total(self, cr, uid, ids, name, args, context):
925 if not ids: return {}
927 for line in self.browse(cr, uid, ids, context=context):
928 res[line.id] = float(line.quantity) * line.amount * line.rate / 100
932 'slip_id':fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade'),
933 'salary_rule_id':fields.many2one('hr.salary.rule', 'Rule', required=True),
934 'employee_id':fields.many2one('hr.employee', 'Employee', required=True),
935 'contract_id':fields.many2one('hr.contract', 'Contract', required=True, select=True),
936 'rate': fields.float('Rate (%)', digits_compute=dp.get_precision('Payroll Rate')),
937 'amount': fields.float('Amount', digits_compute=dp.get_precision('Payroll')),
938 'quantity': fields.float('Quantity', digits_compute=dp.get_precision('Payroll')),
939 'total': fields.function(_calculate_total, method=True, type='float', string='Total', digits_compute=dp.get_precision('Payroll'),store=True ),
949 class hr_payroll_structure(osv.osv):
951 _inherit = 'hr.payroll.structure'
953 'rule_ids':fields.many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id', 'Salary Rules'),
956 hr_payroll_structure()
958 class hr_employee(osv.osv):
963 _inherit = 'hr.employee'
964 _description = 'Employee'
966 def _calculate_total_wage(self, cr, uid, ids, name, args, context):
967 if not ids: return {}
969 current_date = datetime.now().strftime('%Y-%m-%d')
970 for employee in self.browse(cr, uid, ids, context=context):
971 if not employee.contract_ids:
972 res[employee.id] = {'basic': 0.0}
974 cr.execute( 'SELECT SUM(wage) '\
976 'WHERE employee_id = %s '\
977 'AND date_start <= %s '\
978 'AND (date_end > %s OR date_end is NULL)',
979 (employee.id, current_date, current_date))
980 result = dict(cr.dictfetchone())
981 res[employee.id] = {'basic': result['sum']}
985 'slip_ids':fields.one2many('hr.payslip', 'employee_id', 'Payslips', required=False, readonly=True),
986 '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."),
991 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: