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=_("%s (copy)") % (self.browse(cr, uid, id, context=context).code),
86 company_id=self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id)
87 return super(hr_payroll_structure, self).copy(cr, uid, id, default, context=context)
89 def get_all_rules(self, cr, uid, structure_ids, context=None):
91 @param structure_ids: list of structure
92 @return: returns a list of tuple (id, sequence) of rules that are maybe to apply
96 for struct in self.browse(cr, uid, structure_ids, context=context):
97 all_rules += self.pool.get('hr.salary.rule')._recursive_search_of_rules(cr, uid, struct.rule_ids, context=context)
100 def _get_parent_structure(self, cr, uid, struct_ids, context=None):
104 for struct in self.browse(cr, uid, struct_ids, context=context):
106 parent.append(struct.parent_id.id)
108 parent = self._get_parent_structure(cr, uid, parent, context)
109 return parent + struct_ids
111 hr_payroll_structure()
113 class hr_contract(osv.osv):
115 Employee contract based on the visa, work permits
116 allows to configure different Salary structure
119 _inherit = 'hr.contract'
120 _description = 'Employee Contract'
122 'struct_id': fields.many2one('hr.payroll.structure', 'Salary Structure'),
123 'schedule_pay': fields.selection([
124 ('monthly', 'Monthly'),
125 ('quarterly', 'Quarterly'),
126 ('semi-annually', 'Semi-annually'),
127 ('annually', 'Annually'),
128 ('weekly', 'Weekly'),
129 ('bi-weekly', 'Bi-weekly'),
130 ('bi-monthly', 'Bi-monthly'),
131 ], 'Scheduled Pay', select=True),
135 'schedule_pay': 'monthly',
138 def get_all_structures(self, cr, uid, contract_ids, context=None):
140 @param contract_ids: list of contracts
141 @return: the structures linked to the given contracts, ordered by hierachy (parent=False first, then first level children and so on) and without duplicata
144 structure_ids = [contract.struct_id.id for contract in self.browse(cr, uid, contract_ids, context=context)]
145 return list(set(self.pool.get('hr.payroll.structure')._get_parent_structure(cr, uid, structure_ids, context=context)))
149 class contrib_register(osv.osv):
151 Contribution Register
154 _name = 'hr.contribution.register'
155 _description = 'Contribution Register'
158 'company_id':fields.many2one('res.company', 'Company'),
159 'partner_id':fields.many2one('res.partner', 'Partner'),
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 status is \'Draft\'.\
276 \n* If the payslip is under verification, the status is \'Waiting\'. \
277 \n* If the payslip is confirmed then status is set to \'Done\'.\
278 \n* When user cancel payslip the status 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
315 'company_id': company_id,
317 'payslip_run_id': False,
320 return super(hr_payslip, self).copy(cr, uid, id, default, context=context)
322 def cancel_sheet(self, cr, uid, ids, context=None):
323 return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
325 def process_sheet(self, cr, uid, ids, context=None):
326 return self.write(cr, uid, ids, {'paid': True, 'state': 'done'}, context=context)
328 def hr_verify_sheet(self, cr, uid, ids, context=None):
329 self.compute_sheet(cr, uid, ids, context)
330 return self.write(cr, uid, ids, {'state': 'verify'}, context=context)
332 def refund_sheet(self, cr, uid, ids, context=None):
333 mod_obj = self.pool.get('ir.model.data')
334 wf_service = netsvc.LocalService("workflow")
335 for payslip in self.browse(cr, uid, ids, context=context):
336 id_copy = self.copy(cr, uid, payslip.id, {'credit_note': True, 'name': _('Refund: ')+payslip.name}, context=context)
337 self.compute_sheet(cr, uid, [id_copy], context=context)
338 wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'hr_verify_sheet', cr)
339 wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'process_sheet', cr)
341 form_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_form')
342 form_res = form_id and form_id[1] or False
343 tree_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_tree')
344 tree_res = tree_id and tree_id[1] or False
346 'name':_("Refund Payslip"),
347 'view_mode': 'tree, form',
350 'res_model': 'hr.payslip',
351 'type': 'ir.actions.act_window',
354 'domain': "[('id', 'in', %s)]" % [id_copy],
355 'views': [(tree_res, 'tree'), (form_res, 'form')],
359 def check_done(self, cr, uid, ids, context=None):
362 #TODO move this function into hr_contract module, on hr.employee object
363 def get_contract(self, cr, uid, employee, date_from, date_to, context=None):
365 @param employee: browse record of employee
366 @param date_from: date field
367 @param date_to: date field
368 @return: returns the ids of all the contracts for the given employee that need to be considered for the given dates
370 contract_obj = self.pool.get('hr.contract')
372 #a contract is valid if it ends between the given dates
373 clause_1 = ['&',('date_end', '<=', date_to),('date_end','>=', date_from)]
374 #OR if it starts between the given dates
375 clause_2 = ['&',('date_start', '<=', date_to),('date_start','>=', date_from)]
376 #OR if it starts before the date_from and finish after the date_end (or never finish)
377 clause_3 = [('date_start','<=', date_from),'|',('date_end', '=', False),('date_end','>=', date_to)]
378 clause_final = [('employee_id', '=', employee.id),'|','|'] + clause_1 + clause_2 + clause_3
379 contract_ids = contract_obj.search(cr, uid, clause_final, context=context)
382 def compute_sheet(self, cr, uid, ids, context=None):
383 slip_line_pool = self.pool.get('hr.payslip.line')
384 sequence_obj = self.pool.get('ir.sequence')
385 for payslip in self.browse(cr, uid, ids, context=context):
386 number = payslip.number or sequence_obj.get(cr, uid, 'salary.slip')
387 #delete old payslip lines
388 old_slipline_ids = slip_line_pool.search(cr, uid, [('slip_id', '=', payslip.id)], context=context)
391 slip_line_pool.unlink(cr, uid, old_slipline_ids, context=context)
392 if payslip.contract_id:
393 #set the list of contract for which the rules have to be applied
394 contract_ids = [payslip.contract_id.id]
396 #if we don't give the contract, then the rules to apply should be for all current contracts of the employee
397 contract_ids = self.get_contract(cr, uid, payslip.employee_id, payslip.date_from, payslip.date_to, context=context)
398 lines = [(0,0,line) for line in self.pool.get('hr.payslip').get_payslip_lines(cr, uid, contract_ids, payslip.id, context=context)]
399 self.write(cr, uid, [payslip.id], {'line_ids': lines, 'number': number,}, context=context)
402 def get_worked_day_lines(self, cr, uid, contract_ids, date_from, date_to, context=None):
404 @param contract_ids: list of contract id
405 @return: returns a list of dict containing the input that should be applied for the given contract between date_from and date_to
407 def was_on_leave(employee_id, datetime_day, context=None):
409 day = datetime_day.strftime("%Y-%m-%d")
410 holiday_ids = self.pool.get('hr.holidays').search(cr, uid, [('state','=','validate'),('employee_id','=',employee_id),('type','=','remove'),('date_from','<=',day),('date_to','>=',day)])
412 res = self.pool.get('hr.holidays').browse(cr, uid, holiday_ids, context=context)[0].holiday_status_id.name
416 for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
417 if not contract.working_hours:
418 #fill only if the contract as a working schedule linked
421 'name': _("Normal Working Days paid at 100%"),
424 'number_of_days': 0.0,
425 'number_of_hours': 0.0,
426 'contract_id': contract.id,
429 day_from = datetime.strptime(date_from,"%Y-%m-%d")
430 day_to = datetime.strptime(date_to,"%Y-%m-%d")
431 nb_of_days = (day_to - day_from).days + 1
432 for day in range(0, nb_of_days):
433 working_hours_on_day = self.pool.get('resource.calendar').working_hours_on_day(cr, uid, contract.working_hours, day_from + timedelta(days=day), context)
434 if working_hours_on_day:
435 #the employee had to work
436 leave_type = was_on_leave(contract.employee_id.id, day_from + timedelta(days=day), context=context)
438 #if he was on leave, fill the leaves dict
439 if leave_type in leaves:
440 leaves[leave_type]['number_of_days'] += 1.0
441 leaves[leave_type]['number_of_hours'] += working_hours_on_day
443 leaves[leave_type] = {
447 'number_of_days': 1.0,
448 'number_of_hours': working_hours_on_day,
449 'contract_id': contract.id,
452 #add the input vals to tmp (increment if existing)
453 attendances['number_of_days'] += 1.0
454 attendances['number_of_hours'] += working_hours_on_day
455 leaves = [value for key,value in leaves.items()]
456 res += [attendances] + leaves
459 def get_inputs(self, cr, uid, contract_ids, date_from, date_to, context=None):
461 contract_obj = self.pool.get('hr.contract')
462 rule_obj = self.pool.get('hr.salary.rule')
464 structure_ids = contract_obj.get_all_structures(cr, uid, contract_ids, context=context)
465 rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
466 sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
468 for contract in contract_obj.browse(cr, uid, contract_ids, context=context):
469 for rule in rule_obj.browse(cr, uid, sorted_rule_ids, context=context):
471 for input in rule.input_ids:
475 'contract_id': contract.id,
480 def get_payslip_lines(self, cr, uid, contract_ids, payslip_id, context):
481 def _sum_salary_rule_category(localdict, category, amount):
482 if category.parent_id:
483 localdict = _sum_salary_rule_category(localdict, category.parent_id, amount)
484 localdict['categories'].dict[category.code] = category.code in localdict['categories'].dict and localdict['categories'].dict[category.code] + amount or amount
487 class BrowsableObject(object):
488 def __init__(self, pool, cr, uid, employee_id, dict):
492 self.employee_id = employee_id
495 def __getattr__(self, attr):
496 return attr in self.dict and self.dict.__getitem__(attr) or 0.0
498 class InputLine(BrowsableObject):
499 """a class that will be used into the python code, mainly for usability purposes"""
500 def sum(self, code, from_date, to_date=None):
502 to_date = datetime.now().strftime('%Y-%m-%d')
504 self.cr.execute("SELECT sum(amount) as sum\
505 FROM hr_payslip as hp, hr_payslip_input as pi \
506 WHERE hp.employee_id = %s AND hp.state = 'done' \
507 AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s",
508 (self.employee_id, from_date, to_date, code))
509 res = self.cr.fetchone()[0]
512 class WorkedDays(BrowsableObject):
513 """a class that will be used into the python code, mainly for usability purposes"""
514 def _sum(self, code, from_date, to_date=None):
516 to_date = datetime.now().strftime('%Y-%m-%d')
518 self.cr.execute("SELECT sum(number_of_days) as number_of_days, sum(number_of_hours) as number_of_hours\
519 FROM hr_payslip as hp, hr_payslip_worked_days as pi \
520 WHERE hp.employee_id = %s AND hp.state = 'done'\
521 AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s",
522 (self.employee_id, from_date, to_date, code))
523 return self.cr.fetchone()
525 def sum(self, code, from_date, to_date=None):
526 res = self._sum(code, from_date, to_date)
527 return res and res[0] or 0.0
529 def sum_hours(self, code, from_date, to_date=None):
530 res = self._sum(code, from_date, to_date)
531 return res and res[1] or 0.0
533 class Payslips(BrowsableObject):
534 """a class that will be used into the python code, mainly for usability purposes"""
536 def sum(self, code, from_date, to_date=None):
538 to_date = datetime.now().strftime('%Y-%m-%d')
539 self.cr.execute("SELECT sum(case when hp.credit_note = False then (pl.total) else (-pl.total) end)\
540 FROM hr_payslip as hp, hr_payslip_line as pl \
541 WHERE hp.employee_id = %s AND hp.state = 'done' \
542 AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id AND pl.code = %s",
543 (self.employee_id, from_date, to_date, code))
544 res = self.cr.fetchone()
545 return res and res[0] or 0.0
547 #we keep a dict with the result because a value can be overwritten by another rule with the same code
552 payslip_obj = self.pool.get('hr.payslip')
553 inputs_obj = self.pool.get('hr.payslip.worked_days')
554 obj_rule = self.pool.get('hr.salary.rule')
555 payslip = payslip_obj.browse(cr, uid, payslip_id, context=context)
557 for worked_days_line in payslip.worked_days_line_ids:
558 worked_days[worked_days_line.code] = worked_days_line
560 for input_line in payslip.input_line_ids:
561 inputs[input_line.code] = input_line
563 categories_obj = BrowsableObject(self.pool, cr, uid, payslip.employee_id.id, categories_dict)
564 input_obj = InputLine(self.pool, cr, uid, payslip.employee_id.id, inputs)
565 worked_days_obj = WorkedDays(self.pool, cr, uid, payslip.employee_id.id, worked_days)
566 payslip_obj = Payslips(self.pool, cr, uid, payslip.employee_id.id, payslip)
567 rules_obj = BrowsableObject(self.pool, cr, uid, payslip.employee_id.id, rules)
569 localdict = {'categories': categories_obj, 'rules': rules_obj, 'payslip': payslip_obj, 'worked_days': worked_days_obj, 'inputs': input_obj}
570 #get the ids of the structures on the contracts and their parent id as well
571 structure_ids = self.pool.get('hr.contract').get_all_structures(cr, uid, contract_ids, context=context)
572 #get the rules of the structure and thier children
573 rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
574 #run the rules by sequence
575 sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
577 for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
578 employee = contract.employee_id
579 localdict.update({'employee': employee, 'contract': contract})
580 for rule in obj_rule.browse(cr, uid, sorted_rule_ids, context=context):
581 key = rule.code + '-' + str(contract.id)
582 localdict['result'] = None
583 localdict['result_qty'] = 1.0
584 #check if the rule can be applied
585 if obj_rule.satisfy_condition(cr, uid, rule.id, localdict, context=context) and rule.id not in blacklist:
586 #compute the amount of the rule
587 amount, qty, rate = obj_rule.compute_rule(cr, uid, rule.id, localdict, context=context)
588 #check if there is already a rule computed with that code
589 previous_amount = rule.code in localdict and localdict[rule.code] or 0.0
590 #set/overwrite the amount computed for this rule in the localdict
591 tot_rule = amount * qty * rate / 100.0
592 localdict[rule.code] = tot_rule
593 rules[rule.code] = rule
594 #sum the amount for its salary category
595 localdict = _sum_salary_rule_category(localdict, rule.category_id, tot_rule - previous_amount)
596 #create/overwrite the rule in the temporary results
598 'salary_rule_id': rule.id,
599 'contract_id': contract.id,
602 'category_id': rule.category_id.id,
603 'sequence': rule.sequence,
604 'appears_on_payslip': rule.appears_on_payslip,
605 'condition_select': rule.condition_select,
606 'condition_python': rule.condition_python,
607 'condition_range': rule.condition_range,
608 'condition_range_min': rule.condition_range_min,
609 'condition_range_max': rule.condition_range_max,
610 'amount_select': rule.amount_select,
611 'amount_fix': rule.amount_fix,
612 'amount_python_compute': rule.amount_python_compute,
613 'amount_percentage': rule.amount_percentage,
614 'amount_percentage_base': rule.amount_percentage_base,
615 'register_id': rule.register_id.id,
617 'employee_id': contract.employee_id.id,
622 #blacklist this rule and its children
623 blacklist += [id for id, seq in self.pool.get('hr.salary.rule')._recursive_search_of_rules(cr, uid, [rule], context=context)]
625 result = [value for code, value in result_dict.items()]
628 def onchange_employee_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
629 empolyee_obj = self.pool.get('hr.employee')
630 contract_obj = self.pool.get('hr.contract')
631 worked_days_obj = self.pool.get('hr.payslip.worked_days')
632 input_obj = self.pool.get('hr.payslip.input')
636 #delete old worked days lines
637 old_worked_days_ids = ids and worked_days_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
638 if old_worked_days_ids:
639 worked_days_obj.unlink(cr, uid, old_worked_days_ids, context=context)
641 #delete old input lines
642 old_input_ids = ids and input_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
644 input_obj.unlink(cr, uid, old_input_ids, context=context)
650 'input_line_ids': [],
651 'worked_days_line_ids': [],
652 #'details_by_salary_head':[], TODO put me back
654 'contract_id': False,
658 if (not employee_id) or (not date_from) or (not date_to):
660 ttyme = datetime.fromtimestamp(time.mktime(time.strptime(date_from, "%Y-%m-%d")))
661 employee_id = empolyee_obj.browse(cr, uid, employee_id, context=context)
662 res['value'].update({
663 'name': _('Salary Slip of %s for %s') % (employee_id.name, tools.ustr(ttyme.strftime('%B-%Y'))),
664 'company_id': employee_id.company_id.id
667 if not context.get('contract', False):
668 #fill with the first contract of the employee
669 contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
672 #set the list of contract for which the input have to be filled
673 contract_ids = [contract_id]
675 #if we don't give the contract, then the input to fill should be for all current contracts of the employee
676 contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
680 contract_record = contract_obj.browse(cr, uid, contract_ids[0], context=context)
681 res['value'].update({
682 'contract_id': contract_record and contract_record.id or False
684 struct_record = contract_record and contract_record.struct_id or False
685 if not struct_record:
687 res['value'].update({
688 'struct_id': struct_record.id,
690 #computation of the salary input
691 worked_days_line_ids = self.get_worked_day_lines(cr, uid, contract_ids, date_from, date_to, context=context)
692 input_line_ids = self.get_inputs(cr, uid, contract_ids, date_from, date_to, context=context)
693 res['value'].update({
694 'worked_days_line_ids': worked_days_line_ids,
695 'input_line_ids': input_line_ids,
699 def onchange_contract_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
700 #TODO it seems to be the mess in the onchanges, we should have onchange_employee => onchange_contract => doing all the things
708 context.update({'contract': True})
710 res['value'].update({'struct_id': False})
711 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)
715 class hr_payslip_worked_days(osv.osv):
720 _name = 'hr.payslip.worked_days'
721 _description = 'Payslip Worked Days'
723 'name': fields.char('Description', size=256, required=True),
724 'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade', select=True),
725 'sequence': fields.integer('Sequence', required=True, select=True),
726 'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
727 'number_of_days': fields.float('Number of Days'),
728 'number_of_hours': fields.float('Number of Hours'),
729 'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
731 _order = 'payslip_id, sequence'
735 hr_payslip_worked_days()
737 class hr_payslip_input(osv.osv):
742 _name = 'hr.payslip.input'
743 _description = 'Payslip Input'
745 'name': fields.char('Description', size=256, required=True),
746 'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade', select=True),
747 'sequence': fields.integer('Sequence', required=True, select=True),
748 'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
749 '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."),
750 'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
752 _order = 'payslip_id, sequence'
760 class hr_salary_rule(osv.osv):
762 _name = 'hr.salary.rule'
764 'name':fields.char('Name', size=256, required=True, readonly=False),
765 '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."),
766 'sequence': fields.integer('Sequence', required=True, help='Use to arrange calculation sequence', select=True),
767 '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."),
768 'category_id':fields.many2one('hr.salary.rule.category', 'Category', required=True),
769 '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."),
770 'appears_on_payslip': fields.boolean('Appears on Payslip', help="Used to display the salary rule on payslip."),
771 'parent_rule_id':fields.many2one('hr.salary.rule', 'Parent Salary Rule', select=True),
772 'company_id':fields.many2one('res.company', 'Company', required=False),
773 'condition_select': fields.selection([('none', 'Always True'),('range', 'Range'), ('python', 'Python Expression')], "Condition Based on", required=True),
774 '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.'),
775 '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.'),
776 'condition_range_min': fields.float('Minimum Range', required=False, help="The minimum amount, applied for this rule."),
777 'condition_range_max': fields.float('Maximum Range', required=False, help="The maximum amount, applied for this rule."),
778 'amount_select':fields.selection([
779 ('percentage','Percentage (%)'),
780 ('fix','Fixed Amount'),
781 ('code','Python Code'),
782 ],'Amount Type', select=True, required=True, help="The computation method for the rule amount."),
783 'amount_fix': fields.float('Fixed Amount', digits_compute=dp.get_precision('Payroll'),),
784 'amount_percentage': fields.float('Percentage (%)', digits_compute=dp.get_precision('Payroll Rate'), help='For example, enter 50.0 to apply a percentage of 50%'),
785 'amount_python_compute':fields.text('Python Code'),
786 'amount_percentage_base':fields.char('Percentage based on',size=1024, required=False, readonly=False, help='result will be affected to a variable'),
787 'child_ids':fields.one2many('hr.salary.rule', 'parent_rule_id', 'Child Salary Rule'),
788 'register_id':fields.many2one('hr.contribution.register', 'Contribution Register', help="Eventual third party involved in the salary payment of the employees."),
789 'input_ids': fields.one2many('hr.rule.input', 'input_id', 'Inputs'),
790 'note':fields.text('Description'),
793 'amount_python_compute': '''
794 # Available variables:
795 #----------------------
796 # payslip: object containing the payslips
797 # employee: hr.employee object
798 # contract: hr.contract object
799 # rules: object containing the rules code (previously computed)
800 # categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category).
801 # worked_days: object containing the computed worked days.
802 # inputs: object containing the computed inputs.
804 # Note: returned value have to be set in the variable 'result'
806 result = contract.wage * 0.10''',
809 # Available variables:
810 #----------------------
811 # payslip: object containing the payslips
812 # employee: hr.employee object
813 # contract: hr.contract object
814 # rules: object containing the rules code (previously computed)
815 # categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category).
816 # worked_days: object containing the computed worked days
817 # inputs: object containing the computed inputs
819 # Note: returned value have to be set in the variable 'result'
821 result = rules.NET > categories.NET * 0.10''',
822 'condition_range': 'contract.wage',
824 'appears_on_payslip': True,
826 'company_id': lambda self, cr, uid, context: \
827 self.pool.get('res.users').browse(cr, uid, uid,
828 context=context).company_id.id,
829 'condition_select': 'none',
830 'amount_select': 'fix',
832 'amount_percentage': 0.0,
836 def _recursive_search_of_rules(self, cr, uid, rule_ids, context=None):
838 @param rule_ids: list of browse record
839 @return: returns a list of tuple (id, sequence) which are all the children of the passed rule_ids
842 for rule in rule_ids:
844 children_rules += self._recursive_search_of_rules(cr, uid, rule.child_ids, context=context)
845 return [(r.id, r.sequence) for r in rule_ids] + children_rules
847 #TODO should add some checks on the type of result (should be float)
848 def compute_rule(self, cr, uid, rule_id, localdict, context=None):
850 :param rule_id: id of rule to compute
851 :param localdict: dictionary containing the environement in which to compute the rule
852 :return: returns a tuple build as the base/amount computed, the quantity and the rate
853 :rtype: (float, float, float)
855 rule = self.browse(cr, uid, rule_id, context=context)
856 if rule.amount_select == 'fix':
858 return rule.amount_fix, eval(rule.quantity, localdict), 100.0
860 raise osv.except_osv(_('Error!'), _('Wrong quantity defined for salary rule %s (%s).')% (rule.name, rule.code))
861 elif rule.amount_select == 'percentage':
863 return eval(rule.amount_percentage_base, localdict), eval(rule.quantity, localdict), rule.amount_percentage
865 raise osv.except_osv(_('Error!'), _('Wrong percentage base or quantity defined for salary rule %s (%s).')% (rule.name, rule.code))
868 eval(rule.amount_python_compute, localdict, mode='exec', nocopy=True)
869 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
871 raise osv.except_osv(_('Error!'), _('Wrong python code defined for salary rule %s (%s).')% (rule.name, rule.code))
873 def satisfy_condition(self, cr, uid, rule_id, localdict, context=None):
875 @param rule_id: id of hr.salary.rule to be tested
876 @param contract_id: id of hr.contract to be tested
877 @return: returns True if the given rule match the condition for the given contract. Return False otherwise.
879 rule = self.browse(cr, uid, rule_id, context=context)
881 if rule.condition_select == 'none':
883 elif rule.condition_select == 'range':
885 result = eval(rule.condition_range, localdict)
886 return rule.condition_range_min <= result and result <= rule.condition_range_max or False
888 raise osv.except_osv(_('Error!'), _('Wrong range condition defined for salary rule %s (%s).')% (rule.name, rule.code))
891 eval(rule.condition_python, localdict, mode='exec', nocopy=True)
892 return 'result' in localdict and localdict['result'] or False
894 raise osv.except_osv(_('Error!'), _('Wrong python condition defined for salary rule %s (%s).')% (rule.name, rule.code))
898 class hr_rule_input(osv.osv):
903 _name = 'hr.rule.input'
904 _description = 'Salary Rule Input'
906 'name': fields.char('Description', size=256, required=True),
907 'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
908 'input_id': fields.many2one('hr.salary.rule', 'Salary Rule Input', required=True)
913 class hr_payslip_line(osv.osv):
918 _name = 'hr.payslip.line'
919 _inherit = 'hr.salary.rule'
920 _description = 'Payslip Line'
921 _order = 'contract_id, sequence'
923 def _calculate_total(self, cr, uid, ids, name, args, context):
924 if not ids: return {}
926 for line in self.browse(cr, uid, ids, context=context):
927 res[line.id] = float(line.quantity) * line.amount * line.rate / 100
931 'slip_id':fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade'),
932 'salary_rule_id':fields.many2one('hr.salary.rule', 'Rule', required=True),
933 'employee_id':fields.many2one('hr.employee', 'Employee', required=True),
934 'contract_id':fields.many2one('hr.contract', 'Contract', required=True, select=True),
935 'rate': fields.float('Rate (%)', digits_compute=dp.get_precision('Payroll Rate')),
936 'amount': fields.float('Amount', digits_compute=dp.get_precision('Payroll')),
937 'quantity': fields.float('Quantity', digits_compute=dp.get_precision('Payroll')),
938 'total': fields.function(_calculate_total, method=True, type='float', string='Total', digits_compute=dp.get_precision('Payroll'),store=True ),
948 class hr_payroll_structure(osv.osv):
950 _inherit = 'hr.payroll.structure'
952 'rule_ids':fields.many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id', 'Salary Rules'),
955 hr_payroll_structure()
957 class hr_employee(osv.osv):
962 _inherit = 'hr.employee'
963 _description = 'Employee'
965 def _calculate_total_wage(self, cr, uid, ids, name, args, context):
966 if not ids: return {}
968 current_date = datetime.now().strftime('%Y-%m-%d')
969 for employee in self.browse(cr, uid, ids, context=context):
970 if not employee.contract_ids:
971 res[employee.id] = {'basic': 0.0}
973 cr.execute( 'SELECT SUM(wage) '\
975 'WHERE employee_id = %s '\
976 'AND date_start <= %s '\
977 'AND (date_end > %s OR date_end is NULL)',
978 (employee.id, current_date, current_date))
979 result = dict(cr.dictfetchone())
980 res[employee.id] = {'basic': result['sum']}
984 'slip_ids':fields.one2many('hr.payslip', 'employee_id', 'Payslips', required=False, readonly=True),
985 '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."),
990 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: