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'),
160 'partner_id':fields.many2one('res.partner', 'Partner'),
161 'name':fields.char('Name', size=256, required=True, readonly=False),
162 'register_line_ids':fields.one2many('hr.payslip.line', 'register_id', 'Register Line', readonly=True),
163 'note': fields.text('Description'),
166 'company_id': lambda self, cr, uid, context: \
167 self.pool.get('res.users').browse(cr, uid, uid,
168 context=context).company_id.id,
173 class hr_salary_rule_category(osv.osv):
175 HR Salary Rule Category
178 _name = 'hr.salary.rule.category'
179 _description = 'Salary Rule Category'
181 'name':fields.char('Name', size=64, required=True, readonly=False),
182 'code':fields.char('Code', size=64, required=True, readonly=False),
183 'parent_id':fields.many2one('hr.salary.rule.category', 'Parent', help="Linking a salary category to its parent is used only for the reporting purpose."),
184 'children_ids': fields.one2many('hr.salary.rule.category', 'parent_id', 'Children'),
185 'note': fields.text('Description'),
186 'company_id':fields.many2one('res.company', 'Company', required=False),
190 'company_id': lambda self, cr, uid, context: \
191 self.pool.get('res.users').browse(cr, uid, uid,
192 context=context).company_id.id,
195 hr_salary_rule_category()
197 class one2many_mod2(fields.one2many):
199 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
207 ids2 = obj.pool.get(self._obj).search(cr, user, [(self._fields_id,'in',ids), ('appears_on_payslip', '=', True)], limit=self._limit)
208 for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
209 res[r[self._fields_id]].append( r['id'] )
212 class hr_payslip_run(osv.osv):
214 _name = 'hr.payslip.run'
215 _description = 'Payslip Batches'
217 'name': fields.char('Name', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
218 'slip_ids': fields.one2many('hr.payslip', 'payslip_run_id', 'Payslips', required=False, readonly=True, states={'draft': [('readonly', False)]}),
219 'state': fields.selection([
222 ], 'Status', select=True, readonly=True),
223 'date_start': fields.date('Date From', required=True, readonly=True, states={'draft': [('readonly', False)]}),
224 'date_end': fields.date('Date To', required=True, readonly=True, states={'draft': [('readonly', False)]}),
225 '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."),
229 'date_start': lambda *a: time.strftime('%Y-%m-01'),
230 'date_end': lambda *a: str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10],
233 def draft_payslip_run(self, cr, uid, ids, context=None):
234 return self.write(cr, uid, ids, {'state': 'draft'}, context=context)
236 def close_payslip_run(self, cr, uid, ids, context=None):
237 return self.write(cr, uid, ids, {'state': 'close'}, context=context)
241 class hr_payslip(osv.osv):
247 _description = 'Pay Slip'
249 def _get_lines_salary_rule_category(self, cr, uid, ids, field_names, arg=None, context=None):
251 if not ids: return result
253 result.setdefault(id, [])
254 cr.execute('''SELECT pl.slip_id, pl.id FROM hr_payslip_line AS pl \
255 LEFT JOIN hr_salary_rule_category AS sh on (pl.category_id = sh.id) \
256 WHERE pl.slip_id in %s \
257 GROUP BY pl.slip_id, pl.sequence, pl.id ORDER BY pl.sequence''',(tuple(ids),))
260 result[r[0]].append(r[1])
264 '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'),
265 'name': fields.char('Description', size=64, required=False, readonly=True, states={'draft': [('readonly', False)]}),
266 'number': fields.char('Reference', size=64, required=False, readonly=True, states={'draft': [('readonly', False)]}),
267 'employee_id': fields.many2one('hr.employee', 'Employee', required=True, readonly=True, states={'draft': [('readonly', False)]}),
268 'date_from': fields.date('Date From', readonly=True, states={'draft': [('readonly', False)]}, required=True),
269 'date_to': fields.date('Date To', readonly=True, states={'draft': [('readonly', False)]}, required=True),
270 'state': fields.selection([
272 ('verify', 'Waiting'),
274 ('cancel', 'Rejected'),
275 ], 'Status', select=True, readonly=True,
276 help='* When the payslip is created the state is \'Draft\'.\
277 \n* If the payslip is under verification, the state is \'Waiting\'. \
278 \n* If the payslip is confirmed then state is set to \'Done\'.\
279 \n* When user cancel payslip the state is \'Rejected\'.'),
280 # 'line_ids': fields.one2many('hr.payslip.line', 'slip_id', 'Payslip Line', required=False, readonly=True, states={'draft': [('readonly', False)]}),
281 'line_ids': one2many_mod2('hr.payslip.line', 'slip_id', 'Payslip Lines', readonly=True, states={'draft':[('readonly',False)]}),
282 'company_id': fields.many2one('res.company', 'Company', required=False, readonly=True, states={'draft': [('readonly', False)]}),
283 'worked_days_line_ids': fields.one2many('hr.payslip.worked_days', 'payslip_id', 'Payslip Worked Days', required=False, readonly=True, states={'draft': [('readonly', False)]}),
284 'input_line_ids': fields.one2many('hr.payslip.input', 'payslip_id', 'Payslip Inputs', required=False, readonly=True, states={'draft': [('readonly', False)]}),
285 'paid': fields.boolean('Made Payment Order ? ', required=False, readonly=True, states={'draft': [('readonly', False)]}),
286 'note': fields.text('Description', readonly=True, states={'draft':[('readonly',False)]}),
287 'contract_id': fields.many2one('hr.contract', 'Contract', required=False, readonly=True, states={'draft': [('readonly', False)]}),
288 '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'),
289 'credit_note': fields.boolean('Credit Note', help="Indicates this payslip has a refund of another", readonly=True, states={'draft': [('readonly', False)]}),
290 'payslip_run_id': fields.many2one('hr.payslip.run', 'Payslip Batches', readonly=True, states={'draft': [('readonly', False)]}),
293 'date_from': lambda *a: time.strftime('%Y-%m-01'),
294 'date_to': lambda *a: str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10],
296 'credit_note': False,
297 'company_id': lambda self, cr, uid, context: \
298 self.pool.get('res.users').browse(cr, uid, uid,
299 context=context).company_id.id,
302 def _check_dates(self, cr, uid, ids, context=None):
303 for payslip in self.browse(cr, uid, ids, context=context):
304 if payslip.date_from > payslip.date_to:
308 _constraints = [(_check_dates, "Payslip 'Date From' must be before 'Date To'.", ['date_from', 'date_to'])]
310 def copy(self, cr, uid, id, default=None, context=None):
313 company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
318 'company_id': company_id,
320 'basic_before_leaves': 0.0,
323 return super(hr_payslip, self).copy(cr, uid, id, default, context=context)
325 def cancel_sheet(self, cr, uid, ids, context=None):
326 return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
328 def process_sheet(self, cr, uid, ids, context=None):
329 return self.write(cr, uid, ids, {'paid': True, 'state': 'done'}, context=context)
331 def hr_verify_sheet(self, cr, uid, ids, context=None):
332 return self.write(cr, uid, ids, {'state': 'verify'}, context=context)
334 def refund_sheet(self, cr, uid, ids, context=None):
335 mod_obj = self.pool.get('ir.model.data')
336 wf_service = netsvc.LocalService("workflow")
337 for payslip in self.browse(cr, uid, ids, context=context):
338 id_copy = self.copy(cr, uid, payslip.id, {'credit_note': True, 'name': _('Refund: ')+payslip.name}, context=context)
339 self.compute_sheet(cr, uid, [id_copy], context=context)
340 wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'hr_verify_sheet', cr)
341 wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'process_sheet', cr)
343 form_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_form')
344 form_res = form_id and form_id[1] or False
345 tree_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_tree')
346 tree_res = tree_id and tree_id[1] or False
348 'name':_("Refund Payslip"),
349 'view_mode': 'tree, form',
352 'res_model': 'hr.payslip',
353 'type': 'ir.actions.act_window',
356 'domain': "[('id', 'in', %s)]" % [id_copy],
357 'views': [(tree_res, 'tree'), (form_res, 'form')],
361 def check_done(self, cr, uid, ids, context=None):
364 #TODO move this function into hr_contract module, on hr.employee object
365 def get_contract(self, cr, uid, employee, date_from, date_to, context=None):
367 @param employee: browse record of employee
368 @param date_from: date field
369 @param date_to: date field
370 @return: returns the ids of all the contracts for the given employee that need to be considered for the given dates
372 contract_obj = self.pool.get('hr.contract')
374 #a contract is valid if it ends between the given dates
375 clause_1 = ['&',('date_end', '<=', date_to),('date_end','>=', date_from)]
376 #OR if it starts between the given dates
377 clause_2 = ['&',('date_start', '<=', date_to),('date_start','>=', date_from)]
378 #OR if it starts before the date_from and finish after the date_end (or never finish)
379 clause_3 = [('date_start','<=', date_from),'|',('date_end', '=', False),('date_end','>=', date_to)]
380 clause_final = [('employee_id', '=', employee.id),'|','|'] + clause_1 + clause_2 + clause_3
381 contract_ids = contract_obj.search(cr, uid, clause_final, context=context)
384 def compute_sheet(self, cr, uid, ids, context=None):
385 slip_line_pool = self.pool.get('hr.payslip.line')
386 sequence_obj = self.pool.get('ir.sequence')
387 for payslip in self.browse(cr, uid, ids, context=context):
388 number = payslip.number or sequence_obj.get(cr, uid, 'salary.slip')
389 #delete old payslip lines
390 old_slipline_ids = slip_line_pool.search(cr, uid, [('slip_id', '=', payslip.id)], context=context)
393 slip_line_pool.unlink(cr, uid, old_slipline_ids, context=context)
394 if payslip.contract_id:
395 #set the list of contract for which the rules have to be applied
396 contract_ids = [payslip.contract_id.id]
398 #if we don't give the contract, then the rules to apply should be for all current contracts of the employee
399 contract_ids = self.get_contract(cr, uid, payslip.employee_id, payslip.date_from, payslip.date_to, context=context)
400 lines = [(0,0,line) for line in self.pool.get('hr.payslip').get_payslip_lines(cr, uid, contract_ids, payslip.id, context=context)]
401 self.write(cr, uid, [payslip.id], {'line_ids': lines, 'number': number,}, context=context)
404 def get_worked_day_lines(self, cr, uid, contract_ids, date_from, date_to, context=None):
406 @param contract_ids: list of contract id
407 @return: returns a list of dict containing the input that should be applied for the given contract between date_from and date_to
409 def was_on_leave(employee_id, datetime_day, context=None):
411 day = datetime_day.strftime("%Y-%m-%d")
412 holiday_ids = self.pool.get('hr.holidays').search(cr, uid, [('state','=','validate'),('employee_id','=',employee_id),('type','=','remove'),('date_from','<=',day),('date_to','>=',day)])
414 res = self.pool.get('hr.holidays').browse(cr, uid, holiday_ids, context=context)[0].holiday_status_id.name
418 for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
419 if not contract.working_hours:
420 #fill only if the contract as a working schedule linked
423 'name': _("Normal Working Days paid at 100%"),
426 'number_of_days': 0.0,
427 'number_of_hours': 0.0,
428 'contract_id': contract.id,
431 day_from = datetime.strptime(date_from,"%Y-%m-%d")
432 day_to = datetime.strptime(date_to,"%Y-%m-%d")
433 nb_of_days = (day_to - day_from).days + 1
434 for day in range(0, nb_of_days):
435 working_hours_on_day = self.pool.get('resource.calendar').working_hours_on_day(cr, uid, contract.working_hours, day_from + timedelta(days=day), context)
436 if working_hours_on_day:
437 #the employee had to work
438 leave_type = was_on_leave(contract.employee_id.id, day_from + timedelta(days=day), context=context)
440 #if he was on leave, fill the leaves dict
441 if leave_type in leaves:
442 leaves[leave_type]['number_of_days'] += 1.0
443 leaves[leave_type]['number_of_hours'] += working_hours_on_day
445 leaves[leave_type] = {
449 'number_of_days': 1.0,
450 'number_of_hours': working_hours_on_day,
451 'contract_id': contract.id,
454 #add the input vals to tmp (increment if existing)
455 attendances['number_of_days'] += 1.0
456 attendances['number_of_hours'] += working_hours_on_day
457 leaves = [value for key,value in leaves.items()]
458 res += [attendances] + leaves
461 def get_inputs(self, cr, uid, contract_ids, date_from, date_to, context=None):
463 contract_obj = self.pool.get('hr.contract')
464 rule_obj = self.pool.get('hr.salary.rule')
466 structure_ids = contract_obj.get_all_structures(cr, uid, contract_ids, context=context)
467 rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
468 sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
470 for contract in contract_obj.browse(cr, uid, contract_ids, context=context):
471 for rule in rule_obj.browse(cr, uid, sorted_rule_ids, context=context):
473 for input in rule.input_ids:
477 'contract_id': contract.id,
482 def get_payslip_lines(self, cr, uid, contract_ids, payslip_id, context):
483 def _sum_salary_rule_category(localdict, category, amount):
484 if category.parent_id:
485 localdict = _sum_salary_rule_category(localdict, category.parent_id, amount)
486 localdict['categories'].dict[category.code] = category.code in localdict['categories'].dict and localdict['categories'].dict[category.code] + amount or amount
489 class BrowsableObject(object):
490 def __init__(self, pool, cr, uid, employee_id, dict):
494 self.employee_id = employee_id
497 def __getattr__(self, attr):
498 return attr in self.dict and self.dict.__getitem__(attr) or 0.0
500 class InputLine(BrowsableObject):
501 """a class that will be used into the python code, mainly for usability purposes"""
502 def sum(self, code, from_date, to_date=None):
504 to_date = datetime.now().strftime('%Y-%m-%d')
506 self.cr.execute("SELECT sum(amount) as sum\
507 FROM hr_payslip as hp, hr_payslip_input as pi \
508 WHERE hp.employee_id = %s AND hp.state = 'done' \
509 AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s",
510 (self.employee_id, from_date, to_date, code))
511 res = self.cr.fetchone()[0]
514 class WorkedDays(BrowsableObject):
515 """a class that will be used into the python code, mainly for usability purposes"""
516 def _sum(self, code, from_date, to_date=None):
518 to_date = datetime.now().strftime('%Y-%m-%d')
520 self.cr.execute("SELECT sum(number_of_days) as number_of_days, sum(number_of_hours) as number_of_hours\
521 FROM hr_payslip as hp, hr_payslip_worked_days as pi \
522 WHERE hp.employee_id = %s AND hp.state = 'done'\
523 AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s",
524 (self.employee_id, from_date, to_date, code))
525 return self.cr.fetchone()
527 def sum(self, code, from_date, to_date=None):
528 res = self._sum(code, from_date, to_date)
529 return res and res[0] or 0.0
531 def sum_hours(self, code, from_date, to_date=None):
532 res = self._sum(code, from_date, to_date)
533 return res and res[1] or 0.0
535 class Payslips(BrowsableObject):
536 """a class that will be used into the python code, mainly for usability purposes"""
538 def sum(self, code, from_date, to_date=None):
540 to_date = datetime.now().strftime('%Y-%m-%d')
541 self.cr.execute("SELECT sum(case when hp.credit_note = False then (pl.total) else (-pl.total) end)\
542 FROM hr_payslip as hp, hr_payslip_line as pl \
543 WHERE hp.employee_id = %s AND hp.state = 'done' \
544 AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id AND pl.code = %s",
545 (self.employee_id, from_date, to_date, code))
546 res = self.cr.fetchone()
547 return res and res[0] or 0.0
549 #we keep a dict with the result because a value can be overwritten by another rule with the same code
554 payslip_obj = self.pool.get('hr.payslip')
555 inputs_obj = self.pool.get('hr.payslip.worked_days')
556 obj_rule = self.pool.get('hr.salary.rule')
557 payslip = payslip_obj.browse(cr, uid, payslip_id, context=context)
559 for worked_days_line in payslip.worked_days_line_ids:
560 worked_days[worked_days_line.code] = worked_days_line
562 for input_line in payslip.input_line_ids:
563 inputs[input_line.code] = input_line
565 categories_obj = BrowsableObject(self.pool, cr, uid, payslip.employee_id.id, categories_dict)
566 input_obj = InputLine(self.pool, cr, uid, payslip.employee_id.id, inputs)
567 worked_days_obj = WorkedDays(self.pool, cr, uid, payslip.employee_id.id, worked_days)
568 payslip_obj = Payslips(self.pool, cr, uid, payslip.employee_id.id, payslip)
569 rules_obj = BrowsableObject(self.pool, cr, uid, payslip.employee_id.id, rules)
571 localdict = {'categories': categories_obj, 'rules': rules_obj, 'payslip': payslip_obj, 'worked_days': worked_days_obj, 'inputs': input_obj}
572 #get the ids of the structures on the contracts and their parent id as well
573 structure_ids = self.pool.get('hr.contract').get_all_structures(cr, uid, contract_ids, context=context)
574 #get the rules of the structure and thier children
575 rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
576 #run the rules by sequence
577 sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
579 for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
580 employee = contract.employee_id
581 localdict.update({'employee': employee, 'contract': contract})
582 for rule in obj_rule.browse(cr, uid, sorted_rule_ids, context=context):
583 key = rule.code + '-' + str(contract.id)
584 localdict['result'] = None
585 localdict['result_qty'] = 1.0
586 #check if the rule can be applied
587 if obj_rule.satisfy_condition(cr, uid, rule.id, localdict, context=context) and rule.id not in blacklist:
588 #compute the amount of the rule
589 amount, qty, rate = obj_rule.compute_rule(cr, uid, rule.id, localdict, context=context)
590 #check if there is already a rule computed with that code
591 previous_amount = rule.code in localdict and localdict[rule.code] or 0.0
592 #set/overwrite the amount computed for this rule in the localdict
593 tot_rule = amount * qty * rate / 100.0
594 localdict[rule.code] = tot_rule
595 rules[rule.code] = rule
596 #sum the amount for its salary category
597 localdict = _sum_salary_rule_category(localdict, rule.category_id, tot_rule - previous_amount)
598 #create/overwrite the rule in the temporary results
600 'salary_rule_id': rule.id,
601 'contract_id': contract.id,
604 'category_id': rule.category_id.id,
605 'sequence': rule.sequence,
606 'appears_on_payslip': rule.appears_on_payslip,
607 'condition_select': rule.condition_select,
608 'condition_python': rule.condition_python,
609 'condition_range': rule.condition_range,
610 'condition_range_min': rule.condition_range_min,
611 'condition_range_max': rule.condition_range_max,
612 'amount_select': rule.amount_select,
613 'amount_fix': rule.amount_fix,
614 'amount_python_compute': rule.amount_python_compute,
615 'amount_percentage': rule.amount_percentage,
616 'amount_percentage_base': rule.amount_percentage_base,
617 'register_id': rule.register_id.id,
619 'employee_id': contract.employee_id.id,
624 #blacklist this rule and its children
625 blacklist += [id for id, seq in self.pool.get('hr.salary.rule')._recursive_search_of_rules(cr, uid, [rule], context=context)]
627 result = [value for code, value in result_dict.items()]
630 def onchange_employee_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
631 empolyee_obj = self.pool.get('hr.employee')
632 contract_obj = self.pool.get('hr.contract')
633 worked_days_obj = self.pool.get('hr.payslip.worked_days')
634 input_obj = self.pool.get('hr.payslip.input')
638 #delete old worked days lines
639 old_worked_days_ids = ids and worked_days_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
640 if old_worked_days_ids:
641 worked_days_obj.unlink(cr, uid, old_worked_days_ids, context=context)
643 #delete old input lines
644 old_input_ids = ids and input_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
646 input_obj.unlink(cr, uid, old_input_ids, context=context)
652 'input_line_ids': [],
653 'worked_days_line_ids': [],
654 #'details_by_salary_head':[], TODO put me back
656 'contract_id': False,
660 if (not employee_id) or (not date_from) or (not date_to):
662 ttyme = datetime.fromtimestamp(time.mktime(time.strptime(date_from, "%Y-%m-%d")))
663 employee_id = empolyee_obj.browse(cr, uid, employee_id, context=context)
664 res['value'].update({
665 'name': _('Salary Slip of %s for %s') % (employee_id.name, tools.ustr(ttyme.strftime('%B-%Y'))),
666 'company_id': employee_id.company_id.id
669 if not context.get('contract', False):
670 #fill with the first contract of the employee
671 contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
672 res['value'].update({
673 'struct_id': contract_ids and contract_obj.read(cr, uid, contract_ids[0], ['struct_id'], context=context)['struct_id'][0] or False,
674 'contract_id': contract_ids and contract_ids[0] or False,
678 #set the list of contract for which the input have to be filled
679 contract_ids = [contract_id]
680 #fill the structure with the one on the selected contract
681 contract_record = contract_obj.browse(cr, uid, contract_id, context=context)
682 res['value'].update({
683 'struct_id': contract_record.struct_id.id,
684 'contract_id': contract_id
687 #if we don't give the contract, then the input to fill should be for all current contracts of the employee
688 contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
692 #computation of the salary input
693 worked_days_line_ids = self.get_worked_day_lines(cr, uid, contract_ids, date_from, date_to, context=context)
694 input_line_ids = self.get_inputs(cr, uid, contract_ids, date_from, date_to, context=context)
695 res['value'].update({
696 'worked_days_line_ids': worked_days_line_ids,
697 'input_line_ids': input_line_ids,
701 def onchange_contract_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
702 #TODO it seems to be the mess in the onchanges, we should have onchange_employee => onchange_contract => doing all the things
710 context.update({'contract': True})
712 res['value'].update({'struct_id': False})
713 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)
717 class hr_payslip_worked_days(osv.osv):
722 _name = 'hr.payslip.worked_days'
723 _description = 'Payslip Worked Days'
725 'name': fields.char('Description', size=256, required=True),
726 'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade', select=True),
727 'sequence': fields.integer('Sequence', required=True, select=True),
728 'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
729 'number_of_days': fields.float('Number of Days'),
730 'number_of_hours': fields.float('Number of Hours'),
731 'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
733 _order = 'payslip_id, sequence'
737 hr_payslip_worked_days()
739 class hr_payslip_input(osv.osv):
744 _name = 'hr.payslip.input'
745 _description = 'Payslip Input'
747 'name': fields.char('Description', size=256, required=True),
748 'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade', select=True),
749 'sequence': fields.integer('Sequence', required=True, select=True),
750 'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
751 '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."),
752 'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
754 _order = 'payslip_id, sequence'
762 class hr_salary_rule(osv.osv):
764 _name = 'hr.salary.rule'
766 'name':fields.char('Name', size=256, required=True, readonly=False),
767 '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."),
768 'sequence': fields.integer('Sequence', required=True, help='Use to arrange calculation sequence', select=True),
769 '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."),
770 'category_id':fields.many2one('hr.salary.rule.category', 'Category', required=True),
771 '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."),
772 'appears_on_payslip': fields.boolean('Appears on Payslip', help="Used to display the salary rule on payslip."),
773 'parent_rule_id':fields.many2one('hr.salary.rule', 'Parent Salary Rule', select=True),
774 'company_id':fields.many2one('res.company', 'Company', required=False),
775 'condition_select': fields.selection([('none', 'Always True'),('range', 'Range'), ('python', 'Python Expression')], "Condition Based on", required=True),
776 '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.'),
777 '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.'),
778 'condition_range_min': fields.float('Minimum Range', required=False, help="The minimum amount, applied for this rule."),
779 'condition_range_max': fields.float('Maximum Range', required=False, help="The maximum amount, applied for this rule."),
780 'amount_select':fields.selection([
781 ('percentage','Percentage (%)'),
782 ('fix','Fixed Amount'),
783 ('code','Python Code'),
784 ],'Amount Type', select=True, required=True, help="The computation method for the rule amount."),
785 'amount_fix': fields.float('Fixed Amount', digits_compute=dp.get_precision('Payroll'),),
786 'amount_percentage': fields.float('Percentage (%)', digits_compute=dp.get_precision('Payroll Rate'), help='For example, enter 50.0 to apply a percentage of 50%'),
787 'amount_python_compute':fields.text('Python Code'),
788 'amount_percentage_base':fields.char('Percentage based on',size=1024, required=False, readonly=False, help='result will be affected to a variable'),
789 'child_ids':fields.one2many('hr.salary.rule', 'parent_rule_id', 'Child Salary Rule'),
790 'register_id':fields.many2one('hr.contribution.register', 'Contribution Register', help="Eventual third party involved in the salary payment of the employees."),
791 'input_ids': fields.one2many('hr.rule.input', 'input_id', 'Inputs'),
792 'note':fields.text('Description'),
795 'amount_python_compute': '''
796 # Available variables:
797 #----------------------
798 # payslip: object containing the payslips
799 # employee: hr.employee object
800 # contract: hr.contract object
801 # rules: object containing the rules code (previously computed)
802 # categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category).
803 # worked_days: object containing the computed worked days.
804 # inputs: object containing the computed inputs.
806 # Note: returned value have to be set in the variable 'result'
808 result = contract.wage * 0.10''',
811 # Available variables:
812 #----------------------
813 # payslip: object containing the payslips
814 # employee: hr.employee object
815 # contract: hr.contract object
816 # rules: object containing the rules code (previously computed)
817 # categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category).
818 # worked_days: object containing the computed worked days
819 # inputs: object containing the computed inputs
821 # Note: returned value have to be set in the variable 'result'
823 result = rules.NET > categories.NET * 0.10''',
824 'condition_range': 'contract.wage',
826 'appears_on_payslip': True,
828 'company_id': lambda self, cr, uid, context: \
829 self.pool.get('res.users').browse(cr, uid, uid,
830 context=context).company_id.id,
831 'condition_select': 'none',
832 'amount_select': 'fix',
834 'amount_percentage': 0.0,
838 def _recursive_search_of_rules(self, cr, uid, rule_ids, context=None):
840 @param rule_ids: list of browse record
841 @return: returns a list of tuple (id, sequence) which are all the children of the passed rule_ids
844 for rule in rule_ids:
846 children_rules += self._recursive_search_of_rules(cr, uid, rule.child_ids, context=context)
847 return [(r.id, r.sequence) for r in rule_ids] + children_rules
849 #TODO should add some checks on the type of result (should be float)
850 def compute_rule(self, cr, uid, rule_id, localdict, context=None):
852 :param rule_id: id of rule to compute
853 :param localdict: dictionary containing the environement in which to compute the rule
854 :return: returns a tuple build as the base/amount computed, the quantity and the rate
855 :rtype: (float, float, float)
857 rule = self.browse(cr, uid, rule_id, context=context)
858 if rule.amount_select == 'fix':
860 return rule.amount_fix, eval(rule.quantity, localdict), 100.0
862 raise osv.except_osv(_('Error!'), _('Wrong quantity defined for salary rule %s (%s).')% (rule.name, rule.code))
863 elif rule.amount_select == 'percentage':
865 return eval(rule.amount_percentage_base, localdict), eval(rule.quantity, localdict), rule.amount_percentage
867 raise osv.except_osv(_('Error!'), _('Wrong percentage base or quantity defined for salary rule %s (%s).')% (rule.name, rule.code))
870 eval(rule.amount_python_compute, localdict, mode='exec', nocopy=True)
871 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
873 raise osv.except_osv(_('Error!'), _('Wrong python code defined for salary rule %s (%s).')% (rule.name, rule.code))
875 def satisfy_condition(self, cr, uid, rule_id, localdict, context=None):
877 @param rule_id: id of hr.salary.rule to be tested
878 @param contract_id: id of hr.contract to be tested
879 @return: returns True if the given rule match the condition for the given contract. Return False otherwise.
881 rule = self.browse(cr, uid, rule_id, context=context)
883 if rule.condition_select == 'none':
885 elif rule.condition_select == 'range':
887 result = eval(rule.condition_range, localdict)
888 return rule.condition_range_min <= result and result <= rule.condition_range_max or False
890 raise osv.except_osv(_('Error!'), _('Wrong range condition defined for salary rule %s (%s).')% (rule.name, rule.code))
893 eval(rule.condition_python, localdict, mode='exec', nocopy=True)
894 return 'result' in localdict and localdict['result'] or False
896 raise osv.except_osv(_('Error!'), _('Wrong python condition defined for salary rule %s (%s).')% (rule.name, rule.code))
900 class hr_rule_input(osv.osv):
905 _name = 'hr.rule.input'
906 _description = 'Salary Rule Input'
908 'name': fields.char('Description', size=256, required=True),
909 'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
910 'input_id': fields.many2one('hr.salary.rule', 'Salary Rule Input', required=True)
915 class hr_payslip_line(osv.osv):
920 _name = 'hr.payslip.line'
921 _inherit = 'hr.salary.rule'
922 _description = 'Payslip Line'
923 _order = 'contract_id, sequence'
925 def _calculate_total(self, cr, uid, ids, name, args, context):
926 if not ids: return {}
928 for line in self.browse(cr, uid, ids, context=context):
929 res[line.id] = float(line.quantity) * line.amount * line.rate / 100
933 'slip_id':fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade'),
934 'salary_rule_id':fields.many2one('hr.salary.rule', 'Rule', required=True),
935 'employee_id':fields.many2one('hr.employee', 'Employee', required=True),
936 'contract_id':fields.many2one('hr.contract', 'Contract', required=True, select=True),
937 'rate': fields.float('Rate (%)', digits_compute=dp.get_precision('Payroll Rate')),
938 'amount': fields.float('Amount', digits_compute=dp.get_precision('Payroll')),
939 'quantity': fields.float('Quantity', digits_compute=dp.get_precision('Payroll')),
940 'total': fields.function(_calculate_total, method=True, type='float', string='Total', digits_compute=dp.get_precision('Payroll'),store=True ),
950 class hr_payroll_structure(osv.osv):
952 _inherit = 'hr.payroll.structure'
954 'rule_ids':fields.many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id', 'Salary Rules'),
957 hr_payroll_structure()
959 class hr_employee(osv.osv):
964 _inherit = 'hr.employee'
965 _description = 'Employee'
967 def _calculate_total_wage(self, cr, uid, ids, name, args, context):
968 if not ids: return {}
970 current_date = datetime.now().strftime('%Y-%m-%d')
971 for employee in self.browse(cr, uid, ids, context=context):
972 if not employee.contract_ids:
973 res[employee.id] = {'basic': 0.0}
975 cr.execute( 'SELECT SUM(wage) '\
977 'WHERE employee_id = %s '\
978 'AND date_start <= %s '\
979 'AND (date_end > %s OR date_end is NULL)',
980 (employee.id, current_date, current_date))
981 result = dict(cr.dictfetchone())
982 res[employee.id] = {'basic': result['sum']}
986 'slip_ids':fields.one2many('hr.payslip', 'employee_id', 'Payslips', required=False, readonly=True),
987 '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."),
992 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: