2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU Affero General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU Affero General Public License for more details.
18 # You should have received a copy of the GNU Affero General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 ##############################################################################
24 from datetime import date
25 from datetime import datetime
26 from datetime import timedelta
27 from dateutil import relativedelta
30 from osv import fields, osv
32 from tools.translate import _
33 import decimal_precision as dp
35 from tools.safe_eval import safe_eval as eval
37 class hr_payroll_structure(osv.osv):
39 Salary structure used to defined
45 _name = 'hr.payroll.structure'
46 _description = 'Salary Structure'
48 'name':fields.char('Name', size=256, required=True),
49 'code':fields.char('Reference', size=64, required=True),
50 'company_id':fields.many2one('res.company', 'Company', required=True),
51 'note': fields.text('Description'),
52 'parent_id':fields.many2one('hr.payroll.structure', 'Parent'),
55 def _get_parent(self, cr, uid, context=None):
56 obj_model = self.pool.get('ir.model.data')
58 data_id = obj_model.search(cr, uid, [('model', '=', 'hr.payroll.structure'), ('name', '=', 'structure_base')])
60 res = obj_model.browse(cr, uid, data_id[0], context=context).res_id
64 'company_id': lambda self, cr, uid, context: \
65 self.pool.get('res.users').browse(cr, uid, uid,
66 context=context).company_id.id,
67 'parent_id': _get_parent,
70 def copy(self, cr, uid, id, default=None, context=None):
72 Create a new record in hr_payroll_structure model from existing one
73 @param cr: cursor to database
74 @param user: id of current user
75 @param id: list of record ids on which copy method executes
76 @param default: dict type contains the values to be override during copy of object
77 @param context: context arguments, like lang, time zone
79 @return: returns a id of newly created record
84 'code': self.browse(cr, uid, id, context=context).code + "(copy)",
85 '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', required=True),
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),
134 def get_all_structures(self, cr, uid, contract_ids, context=None):
136 @param contract_ids: list of contracts
137 @return: the structures linked to the given contracts, ordered by hierachy (parent=False first, then first level children and so on) and without duplicata
140 structure_ids = [contract.struct_id.id for contract in self.browse(cr, uid, contract_ids, context=context)]
141 return list(set(self.pool.get('hr.payroll.structure')._get_parent_structure(cr, uid, structure_ids, context=context)))
145 class contrib_register(osv.osv):
147 Contribution Register
150 _name = 'hr.contribution.register'
151 _description = 'Contribution Register'
154 'company_id':fields.many2one('res.company', 'Company', required=False),
155 'name':fields.char('Name', size=256, required=True, readonly=False),
156 'register_line_ids':fields.one2many('hr.payslip.line', 'register_id', 'Register Line', readonly=True),
157 'note': fields.text('Description'),
160 'company_id': lambda self, cr, uid, context: \
161 self.pool.get('res.users').browse(cr, uid, uid,
162 context=context).company_id.id,
167 class hr_salary_rule_category(osv.osv):
169 HR Salary Rule Category
172 _name = 'hr.salary.rule.category'
173 _description = 'Salary Rule Category'
175 'name':fields.char('Name', size=64, required=True, readonly=False),
176 'code':fields.char('Code', size=64, required=True, readonly=False),
177 'parent_id':fields.many2one('hr.salary.rule.category', 'Parent', help="Linking a salary category to its parent is used only for the reporting purpose."),
178 'note': fields.text('Description'),
179 'company_id':fields.many2one('res.company', 'Company', required=False),
183 'company_id': lambda self, cr, uid, context: \
184 self.pool.get('res.users').browse(cr, uid, uid,
185 context=context).company_id.id,
188 hr_salary_rule_category()
190 class one2many_mod2(fields.one2many):
192 def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
200 ids2 = obj.pool.get(self._obj).search(cr, user, [(self._fields_id,'in',ids), ('appears_on_payslip', '=', True)], limit=self._limit)
201 for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
202 res[r[self._fields_id]].append( r['id'] )
205 class hr_payslip_run(osv.osv):
207 _name = 'hr.payslip.run'
209 'name': fields.char('Name', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
210 'slip_ids': fields.one2many('hr.payslip', 'payslip_run_id', 'Payslips', required=False, readonly=True, states={'draft': [('readonly', False)]}),
211 'state': fields.selection([
214 ], 'State', select=True, readonly=True)
220 def draft_payslip_run(self, cr, uid, ids, context=None):
221 return self.write(cr, uid, ids, {'state': 'draft'}, context=context)
223 def close_payslip_run(self, cr, uid, ids, context=None):
224 return self.write(cr, uid, ids, {'state': 'close'}, context=context)
228 class hr_payslip(osv.osv):
234 _description = 'Pay Slip'
236 def _get_lines_salary_rule_category(self, cr, uid, ids, field_names, arg=None, context=None):
238 if not ids: return result
240 result.setdefault(id, [])
241 cr.execute('''SELECT pl.slip_id, pl.id FROM hr_payslip_line AS pl \
242 LEFT JOIN hr_salary_rule_category AS sh on (pl.category_id = sh.id) \
243 WHERE pl.slip_id in %s \
244 GROUP BY pl.slip_id, pl.sequence, pl.id ORDER BY pl.sequence''',(tuple(ids),))
247 result[r[0]].append(r[1])
251 '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'),
252 'name': fields.char('Description', size=64, required=False, readonly=True, states={'draft': [('readonly', False)]}),
253 'number': fields.char('Reference', size=64, required=False, readonly=True, states={'draft': [('readonly', False)]}),
254 'employee_id': fields.many2one('hr.employee', 'Employee', required=True, readonly=True, states={'draft': [('readonly', False)]}),
255 'date_from': fields.date('Date From', readonly=True, states={'draft': [('readonly', False)]}, required=True),
256 'date_to': fields.date('Date To', readonly=True, states={'draft': [('readonly', False)]}, required=True),
257 'state': fields.selection([
259 ('verify', 'Waiting'),
261 ('cancel', 'Rejected'),
262 ], 'State', select=True, readonly=True,
263 help='* When the payslip is created the state is \'Draft\'.\
264 \n* If the payslip is under verification, the state is \'Waiting\'. \
265 \n* If the payslip is confirmed then state is set to \'Done\'.\
266 \n* When user cancel payslip the state is \'Rejected\'.'),
267 # 'line_ids': fields.one2many('hr.payslip.line', 'slip_id', 'Payslip Line', required=False, readonly=True, states={'draft': [('readonly', False)]}),
268 'line_ids': one2many_mod2('hr.payslip.line', 'slip_id', 'Payslip Lines', readonly=True, states={'draft':[('readonly',False)]}),
269 'company_id': fields.many2one('res.company', 'Company', required=False, readonly=True, states={'draft': [('readonly', False)]}),
270 'worked_days_line_ids': fields.one2many('hr.payslip.worked_days', 'payslip_id', 'Payslip Worked Days', required=False, readonly=True, states={'draft': [('readonly', False)]}),
271 'input_line_ids': fields.one2many('hr.payslip.input', 'payslip_id', 'Payslip Inputs', required=False, readonly=True, states={'draft': [('readonly', False)]}),
272 'paid': fields.boolean('Made Payment Order ? ', required=False, readonly=True, states={'draft': [('readonly', False)]}),
273 'note': fields.text('Description', readonly=True, states={'draft':[('readonly',False)]}),
274 'contract_id': fields.many2one('hr.contract', 'Contract', required=False, readonly=True, states={'draft': [('readonly', False)]}),
275 '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'),
276 'credit_note': fields.boolean('Credit Note', help="Indicates this payslip has a refund of another"),
277 'payslip_run_id': fields.many2one('hr.payslip.run', 'Payslip Run', readonly=True, states={'draft': [('readonly', False)]}),
280 'date_from': lambda *a: time.strftime('%Y-%m-01'),
281 'date_to': lambda *a: str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10],
283 'credit_note': False,
284 'company_id': lambda self, cr, uid, context: \
285 self.pool.get('res.users').browse(cr, uid, uid,
286 context=context).company_id.id,
289 def copy(self, cr, uid, id, default=None, context=None):
292 company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
297 'company_id': company_id,
299 'basic_before_leaves': 0.0,
302 return super(hr_payslip, self).copy(cr, uid, id, default, context=context)
304 def cancel_sheet(self, cr, uid, ids, context=None):
305 return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
307 def account_check_sheet(self, cr, uid, ids, context=None):
308 return self.write(cr, uid, ids, {'state': 'accont_check'}, context=context)
310 def hr_check_sheet(self, cr, uid, ids, context=None):
311 return self.write(cr, uid, ids, {'state': 'hr_check'}, context=context)
313 def process_sheet(self, cr, uid, ids, context=None):
314 return self.write(cr, uid, ids, {'paid': True, 'state': 'done'}, context=context)
316 def hr_verify_sheet(self, cr, uid, ids, context=None):
317 return self.write(cr, uid, ids, {'state': 'verify'}, context=context)
319 def refund_sheet(self, cr, uid, ids, context=None):
320 mod_obj = self.pool.get('ir.model.data')
321 wf_service = netsvc.LocalService("workflow")
323 id_copy = self.copy(cr, uid, id, {'credit_note': True}, context=context)
324 self.compute_sheet(cr, uid, [id_copy], context=context)
325 wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'hr_verify_sheet', cr)
326 wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'process_sheet', cr)
328 form_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_form')
329 form_res = form_id and form_id[1] or False
330 tree_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_tree')
331 tree_res = tree_id and tree_id[1] or False
333 'name':_("Refund Payslip"),
334 'view_mode': 'tree, form',
337 'res_model': 'hr.payslip',
338 'type': 'ir.actions.act_window',
341 'domain': "[('id', 'in', %s)]" % [id_copy],
342 'views': [(tree_res, 'tree'), (form_res, 'form')],
346 def verify_sheet(self, cr, uid, ids, context=None):
347 return self.write(cr, uid, ids, {'state': 'confirm'}, context=context)
349 def check_done(self, cr, uid, ids, context=None):
352 #TODO move this function into hr_contract module, on hr.employee object
353 def get_contract(self, cr, uid, employee, date_from, date_to, context=None):
355 @param employee: browse record of employee
356 @param date_from: date field
357 @param date_to: date field
358 @return: returns the ids of all the contracts for the given employee that need to be considered for the given dates
360 contract_obj = self.pool.get('hr.contract')
362 #a contract is valid if it ends between the given dates
363 clause_1 = ['&',('date_end', '<=', date_to),('date_end','>=', date_from)]
364 #OR if it starts between the given dates
365 clause_2 = ['&',('date_start', '<=', date_to),('date_start','>=', date_from)]
366 #OR if it starts before the date_from and finish after the date_end (or never finish)
367 clause_3 = [('date_start','<=', date_from),'|',('date_end', '=', False),('date_end','>=', date_to)]
368 clause_final = [('employee_id', '=', employee.id),'|','|'] + clause_1 + clause_2 + clause_3
369 contract_ids = contract_obj.search(cr, uid, clause_final, context=context)
372 def compute_sheet(self, cr, uid, ids, context=None):
373 slip_line_pool = self.pool.get('hr.payslip.line')
374 sequence_obj = self.pool.get('ir.sequence')
375 for payslip in self.browse(cr, uid, ids, context=context):
376 number = sequence_obj.get(cr, uid, 'salary.slip')
377 #delete old payslip lines
378 old_slipline_ids = slip_line_pool.search(cr, uid, [('slip_id', '=', payslip.id)], context=context)
381 slip_line_pool.unlink(cr, uid, old_slipline_ids, context=context)
382 if payslip.contract_id:
383 #set the list of contract for which the rules have to be applied
384 contract_ids = [payslip.contract_id.id]
386 #if we don't give the contract, then the rules to apply should be for all current contracts of the employee
387 contract_ids = self.get_contract(cr, uid, payslip.employee_id, payslip.date_from, payslip.date_to, context=context)
388 lines = [(0,0,line) for line in self.pool.get('hr.payslip').get_payslip_lines(cr, uid, contract_ids, payslip.id, context=context)]
389 self.write(cr, uid, [payslip.id], {'line_ids': lines, 'number': number,}, context=context)
392 def get_worked_day_lines(self, cr, uid, contract_ids, date_from, date_to, context=None):
394 @param contract_ids: list of contract id
395 @return: returns a list of dict containing the input that should be applied for the given contract between date_from and date_to
397 def was_on_leave(employee_id, datetime_day, context=None):
399 day = datetime_day.strftime("%Y-%m-%d")
400 holiday_ids = self.pool.get('hr.holidays').search(cr, uid, [('state','=','validate'),('employee_id','=',employee_id),('type','=','remove'),('date_from','<=',day),('date_to','>=',day)])
402 res = self.pool.get('hr.holidays').browse(cr, uid, holiday_ids, context=context)[0].holiday_status_id.name
406 for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
407 if not contract.working_hours:
408 #fill only if the contract as a working schedule linked
411 'name': _("Normal Working Days paid at 100%"),
414 'number_of_days': 0.0,
415 'number_of_hours': 0.0,
416 'contract_id': contract.id,
419 day_from = datetime.strptime(date_from,"%Y-%m-%d")
420 day_to = datetime.strptime(date_to,"%Y-%m-%d")
421 nb_of_days = (day_to - day_from).days + 1
422 for day in range(0, nb_of_days):
423 working_hours_on_day = self.pool.get('resource.calendar').working_hours_on_day(cr, uid, contract.working_hours, day_from + timedelta(days=day), context)
424 if working_hours_on_day:
425 #the employee had to work
426 leave_type = was_on_leave(contract.employee_id.id, day_from + timedelta(days=day), context=context)
428 #if he was on leave, fill the leaves dict
429 if leave_type in leaves:
430 leaves[leave_type]['number_of_days'] += 1.0
431 leaves[leave_type]['number_of_hours'] += working_hours_on_day
433 leaves[leave_type] = {
437 'number_of_days': 1.0,
438 'number_of_hours': working_hours_on_day,
439 'contract_id': contract.id,
442 #add the input vals to tmp (increment if existing)
443 attendances['number_of_days'] += 1.0
444 attendances['number_of_hours'] += working_hours_on_day
445 leaves = [value for key,value in leaves.items()]
446 res += [attendances] + leaves
449 def get_inputs(self, cr, uid, contract_ids, date_from, date_to, context=None):
451 contract_obj = self.pool.get('hr.contract')
452 rule_obj = self.pool.get('hr.salary.rule')
454 structure_ids = contract_obj.get_all_structures(cr, uid, contract_ids, context=context)
455 rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
456 sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
458 for contract in contract_obj.browse(cr, uid, contract_ids, context=context):
459 for rule in rule_obj.browse(cr, uid, sorted_rule_ids, context=context):
461 for input in rule.input_ids:
465 'contract_id': contract.id,
470 def get_payslip_lines(self, cr, uid, contract_ids, payslip_id, context):
471 def _sum_salary_rule_category(localdict, category, amount):
472 if category.parent_id:
473 localdict = _sum_salary_rule_category(localdict, category.parent_id, amount)
474 localdict['categories'][category.code] = category.code in localdict['categories'] and localdict['categories'][category.code] + amount or amount
477 class BrowsableObject(object):
478 def __init__(self, pool, cr, uid, employee_id, dict):
482 self.employee_id = employee_id
485 def __getattr__(self, attr):
486 return self.dict.__getitem__(attr)
488 class InputLine(BrowsableObject):
489 """a class that will be used into the python code, mainly for usability purposes"""
490 def sum(self, code, from_date, to_date=None):
492 to_date = datetime.now().strftime('%Y-%m-%d')
494 self.cr.execute("SELECT sum(quantity) as sum\
495 FROM hr_payslip as hp, hr_payslip_input as pi \
496 WHERE hp.employee_id = %s AND hp.state in ('confirm','done') \
497 AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s",
498 (self.employee_id, from_date, to_date, code))
499 res = self.cr.fetchone()[0]
502 class WorkedDays(BrowsableObject):
503 """a class that will be used into the python code, mainly for usability purposes"""
504 def _sum(self, code, from_date, to_date=None):
506 to_date = datetime.now().strftime('%Y-%m-%d')
508 self.cr.execute("SELECT sum(number_of_days) as number_of_days, sum(number_of_hours) as number_of_hours\
509 FROM hr_payslip as hp, hr_payslip_worked_days as pi \
510 WHERE hp.employee_id = %s AND hp.state in ('confirm','done') \
511 AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s",
512 (self.employee_id, from_date, to_date, code))
513 return self.cr.fetchone()
515 def sum(self, code, from_date, to_date=None):
516 res = self._sum(code, from_date, to_date)
517 return res and res[0] or 0.0
519 def sum_hours(self, code, from_date, to_date=None):
520 res = self._sum(code, from_date, to_date)
521 return res and res[1] or 0.0
523 class Payslips(BrowsableObject):
524 """a class that will be used into the python code, mainly for usability purposes"""
526 def sum(self, code, from_date, to_date=None):
528 to_date = datetime.now().strftime('%Y-%m-%d')
529 self.cr.execute("SELECT sum(case when hp.credit_note = False then (pl.total) else (-pl.total) end)\
530 FROM hr_payslip as hp, hr_payslip_line as pl \
531 WHERE hp.employee_id = %s AND hp.state in ('confirm','done') \
532 AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id AND pl.code = %s",
533 (self.employee_id, from_date, to_date, code))
534 res = self.cr.fetchone()
535 return res and res[0] or 0.0
537 #we keep a dict with the result because a value can be overwritten by another rule with the same code
540 payslip_obj = self.pool.get('hr.payslip')
541 inputs_obj = self.pool.get('hr.payslip.worked_days')
542 obj_rule = self.pool.get('hr.salary.rule')
543 payslip = payslip_obj.browse(cr, uid, payslip_id, context=context)
545 for worked_days_line in payslip.worked_days_line_ids:
546 worked_days[worked_days_line.code] = worked_days_line
548 for input_line in payslip.input_line_ids:
549 inputs[input_line.code] = input_line
551 input_obj = InputLine(self.pool, cr, uid, payslip.employee_id.id, inputs)
552 worked_days_obj = WorkedDays(self.pool, cr, uid, payslip.employee_id.id, worked_days)
553 payslip_obj = Payslips(self.pool, cr, uid, payslip.employee_id.id, payslip)
555 localdict = {'categories': {}, 'payslip': payslip_obj, 'worked_days': worked_days_obj, 'inputs': input_obj}
556 #get the ids of the structures on the contracts and their parent id as well
557 structure_ids = self.pool.get('hr.contract').get_all_structures(cr, uid, contract_ids, context=context)
558 #get the rules of the structure and thier children
559 rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
560 #run the rules by sequence
561 sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
563 for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
564 employee = contract.employee_id
565 localdict.update({'employee': employee, 'contract': contract})
566 for rule in obj_rule.browse(cr, uid, sorted_rule_ids, context=context):
567 key = rule.code + '-' + str(contract.id)
568 localdict['result'] = None
569 #check if the rule can be applied
570 if obj_rule.satisfy_condition(cr, uid, rule.id, localdict, context=context) and rule.id not in blacklist:
571 #compute the amount of the rule
572 amount = obj_rule.compute_rule(cr, uid, rule.id, localdict, context=context)
573 #check if there is already a rule computed with that code
574 previous_amount = rule.code in localdict and localdict[rule.code] or 0.0
575 #set/overwrite the amount computed for this rule in the localdict
576 localdict[rule.code] = amount
577 #sum the amount for its salary category
578 localdict = _sum_salary_rule_category(localdict, rule.category_id, amount - previous_amount)
579 #create/overwrite the rule in the temporary results
581 'salary_rule_id': rule.id,
582 'contract_id': contract.id,
585 'category_id': rule.category_id.id,
586 'sequence': rule.sequence,
587 'appears_on_payslip': rule.appears_on_payslip,
588 'condition_select': rule.condition_select,
589 'condition_python': rule.condition_python,
590 'condition_range': rule.condition_range,
591 'condition_range_min': rule.condition_range_min,
592 'condition_range_max': rule.condition_range_max,
593 'amount_select': rule.amount_select,
594 'amount_fix': rule.amount_fix,
595 'amount_python_compute': rule.amount_python_compute,
596 'amount_percentage': rule.amount_percentage,
597 'amount_percentage_base': rule.amount_percentage_base,
598 'register_id': rule.register_id.id,
600 'employee_id': contract.employee_id.id,
601 'quantity': rule.quantity,
604 #blacklist this rule and its children
605 blacklist += [id for id, seq in self.pool.get('hr.salary.rule')._recursive_search_of_rules(cr, uid, [rule], context=context)]
607 result = [value for code, value in result_dict.items()]
610 def onchange_employee_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
611 empolyee_obj = self.pool.get('hr.employee')
612 contract_obj = self.pool.get('hr.contract')
613 worked_days_obj = self.pool.get('hr.payslip.worked_days')
614 input_obj = self.pool.get('hr.payslip.input')
618 #delete old worked days lines
619 old_worked_days_ids = ids and worked_days_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
620 if old_worked_days_ids:
621 worked_days_obj.unlink(cr, uid, old_worked_days_ids, context=context)
623 #delete old input lines
624 old_input_ids = ids and input_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
626 input_obj.unlink(cr, uid, old_input_ids, context=context)
632 'input_line_ids': [],
633 'worked_days_line_ids': [],
634 #'details_by_salary_head':[], TODO put me back
636 'contract_id': False,
642 ttyme = datetime.fromtimestamp(time.mktime(time.strptime(date_from, "%Y-%m-%d")))
643 employee_id = empolyee_obj.browse(cr, uid, employee_id, context=context)
644 res['value'].update({
645 'name': _('Salary Slip of %s for %s') % (employee_id.name, tools.ustr(ttyme.strftime('%B-%Y'))),
646 'company_id': employee_id.company_id.id
649 if not context.get('contract', False):
650 #fill with the first contract of the employee
651 contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
652 res['value'].update({
653 'struct_id': contract_ids and contract_obj.read(cr, uid, contract_ids[0], ['struct_id'], context=context)['struct_id'][0] or False,
654 'contract_id': contract_ids and contract_ids[0] or False,
658 #set the list of contract for which the input have to be filled
659 contract_ids = [contract_id]
660 #fill the structure with the one on the selected contract
661 contract_record = contract_obj.browse(cr, uid, contract_id, context=context)
662 res['value'].update({
663 'struct_id': contract_record.struct_id.id,
664 'contract_id': contract_id
667 #if we don't give the contract, then the input to fill should be for all current contracts of the employee
668 contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
672 #computation of the salary input
673 worked_days_line_ids = self.get_worked_day_lines(cr, uid, contract_ids, date_from, date_to, context=context)
674 input_line_ids = self.get_inputs(cr, uid, contract_ids, date_from, date_to, context=context)
675 res['value'].update({
676 'worked_days_line_ids': worked_days_line_ids,
677 'input_line_ids': input_line_ids,
681 def onchange_contract_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
682 #TODO it seems to be the mess in the onchanges, we should have onchange_employee => onchange_contract => doing all the things
690 context.update({'contract': True})
692 res['value'].update({'struct_id': False})
693 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)
697 class hr_payslip_worked_days(osv.osv):
702 _name = 'hr.payslip.worked_days'
703 _description = 'Payslip Worked Days'
705 'name': fields.char('Description', size=256, required=True),
706 'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True),
707 'sequence': fields.integer('Sequence', required=True,),
708 'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
709 'number_of_days': fields.float('Number of Days'),
710 'number_of_hours': fields.float('Number of Hours'),
711 'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
713 _order = 'payslip_id, sequence'
717 hr_payslip_worked_days()
719 class hr_payslip_input(osv.osv):
724 _name = 'hr.payslip.input'
725 _description = 'Payslip Input'
727 'name': fields.char('Description', size=256, required=True),
728 'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True),
729 'sequence': fields.integer('Sequence', required=True,),
730 'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
731 'quantity': fields.float('Quantity', 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.SASUS.qunatity * contract.wage*0.01."),
732 'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
734 _order = 'payslip_id, sequence'
742 class hr_salary_rule(osv.osv):
744 _name = 'hr.salary.rule'
746 'name':fields.char('Name', size=256, required=True, readonly=False),
747 '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."),
748 'sequence': fields.integer('Sequence', required=True, help='Use to arrange calculation sequence'),
749 '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."),
750 'category_id':fields.many2one('hr.salary.rule.category', 'Category', required=True),
751 '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."),
752 'appears_on_payslip': fields.boolean('Appears on Payslip', help="Used for the display of rule on payslip"),
753 'parent_rule_id':fields.many2one('hr.salary.rule', 'Parent Salary Rule', select=True),
754 'company_id':fields.many2one('res.company', 'Company', required=False),
755 'condition_select': fields.selection([('none', 'Always True'),('range', 'Range'), ('python', 'Python Expression')], "Condition Based on", required=True),
756 'condition_range':fields.char('Range Based on',size=1024, readonly=False, help='This will use to computer the % fields values, in general its on basic, but You can use all categories code field in small letter as a variable name i.e. hra, ma, lta, etc...., also you can use, static varible basic'),
757 '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
758 'condition_range_min': fields.float('Minimum Range', required=False, help="The minimum amount, applied for this rule."),
759 'condition_range_max': fields.float('Maximum Range', required=False, help="The maximum amount, applied for this rule."),
760 'amount_select':fields.selection([
761 ('percentage','Percentage (%)'),
762 ('fix','Fixed Amount'),
763 ('code','Python Code'),
764 ],'Amount Type', select=True, required=True, help="The computation method for the rule amount."),
765 'amount_fix': fields.float('Fixed Amount', digits_compute=dp.get_precision('Account'),),
766 'amount_percentage': fields.float('Percentage (%)', digits_compute=dp.get_precision('Account'), help='For example, enter 50.0 to apply a percentage of 50%'),
767 'amount_python_compute':fields.text('Python Code'),
768 'amount_percentage_base':fields.char('Percentage based on',size=1024, required=False, readonly=False, help='result will be affected to a variable'),
769 'child_ids':fields.one2many('hr.salary.rule', 'parent_rule_id', 'Child Salary Rule'),
770 'register_id':fields.property(
771 'hr.contribution.register',
773 relation='hr.contribution.register',
774 string="Contribution Register",
777 help="Contribution register based on company",
780 'input_ids': fields.one2many('hr.rule.input', 'input_id', 'Inputs'),
781 'note':fields.text('Description'),
784 'amount_python_compute': '''
785 # Available variables:
786 #----------------------
787 # payslip: object containing the payslips
788 # employee: hr.employee object
789 # contract: hr.contract object
790 # rules: rules code (previously computed)
791 # categories: dictionary containing the computed salary rule categories (sum of amount of all rules belonging to that category). Keys are the category codes.
792 # worked_days: object containing the computed worked days.
793 # inputs: object containing the computed inputs.
795 # Note: returned value have to be set in the variable 'result'
797 result = contract.wage * 0.10''',
800 # Available variables:
801 #----------------------
802 # payslip: object containing the payslips
803 # employee: hr.employee object
804 # contract: hr.contract object
805 # rules: rules code (previously computed)
806 # categories: dictionary containing the computed salary rule categories (sum of amount of all rules belonging to that category). Keys are the category codes.
807 # worked_days: object containing the computed worked days
808 # inputs: object containing the computed inputs
810 # Note: returned value have to be set in the variable 'result'
812 result = rules['NET'] > categories['NET'] * 0.10''',
813 'condition_range': 'contract.wage',
815 'appears_on_payslip': True,
817 'company_id': lambda self, cr, uid, context: \
818 self.pool.get('res.users').browse(cr, uid, uid,
819 context=context).company_id.id,
820 'condition_select': 'none',
821 'amount_select': 'fix',
823 'amount_percentage': 0.0,
827 def _recursive_search_of_rules(self, cr, uid, rule_ids, context=None):
829 @param rule_ids: list of browse record
830 @return: returns a list of tuple (id, sequence) which are all the children of the passed rule_ids
833 for rule in rule_ids:
835 children_rules += self._recursive_search_of_rules(cr, uid, rule.child_ids, context=context)
836 return [(r.id, r.sequence) for r in rule_ids] + children_rules
838 #TODO should add some checks on the type of result (should be float)
839 def compute_rule(self, cr, uid, rule_id, localdict, context=None):
841 @param rule_id: id of rule to compute
842 @param localdict: dictionary containing the environement in which to compute the rule
843 @return: returns the result of computation as float
845 rule = self.browse(cr, uid, rule_id, context=context)
846 if rule.amount_select == 'fix':
848 return rule.amount_fix * eval(rule.quantity, localdict)
850 raise osv.except_osv(_('Error'), _('Wrong quantity defined for salary rule %s (%s)')% (rule.name, rule.code))
851 elif rule.amount_select == 'percentage':
853 amount = rule.amount_percentage * eval(rule.amount_percentage_base, localdict) / 100
854 return amount * eval(rule.quantity, localdict)
856 raise osv.except_osv(_('Error'), _('Wrong percentage base or quantity defined for salary rule %s (%s)')% (rule.name, rule.code))
859 eval(rule.amount_python_compute, localdict, mode='exec', nocopy=True)
860 return localdict['result']
862 raise osv.except_osv(_('Error'), _('Wrong python code defined for salary rule %s (%s) ')% (rule.name, rule.code))
864 def satisfy_condition(self, cr, uid, rule_id, localdict, context=None):
866 @param rule_id: id of hr.salary.rule to be tested
867 @param contract_id: id of hr.contract to be tested
868 @return: returns True if the given rule match the condition for the given contract. Return False otherwise.
870 rule = self.browse(cr, uid, rule_id, context=context)
872 if rule.condition_select == 'none':
874 elif rule.condition_select == 'range':
876 result = eval(rule.condition_range, localdict)
877 return rule.condition_range_min <= result and result <= rule.condition_range_max or False
879 raise osv.except_osv(_('Error'), _('Wrong range condition defined for salary rule %s (%s)')% (rule.name, rule.code))
882 eval(rule.condition_python, localdict, mode='exec', nocopy=True)
883 return 'result' in localdict and localdict['result'] or False
885 raise osv.except_osv(_('Error'), _('Wrong python condition defined for salary rule %s (%s)')% (rule.name, rule.code))
889 class hr_rule_input(osv.osv):
894 _name = 'hr.rule.input'
895 _description = 'Salary Rule Input'
897 'name': fields.char('Description', size=256, required=True),
898 'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
899 'input_id': fields.many2one('hr.salary.rule', 'Salary Rule Input', required=True)
904 class hr_payslip_line(osv.osv):
909 _name = 'hr.payslip.line'
910 _inherit = 'hr.salary.rule'
911 _description = 'Payslip Line'
912 _order = 'contract_id, sequence'
915 'slip_id':fields.many2one('hr.payslip', 'Pay Slip', required=True),
916 'salary_rule_id':fields.many2one('hr.salary.rule', 'Rule', required=True),
917 'employee_id':fields.many2one('hr.employee', 'Employee', required=True),
918 'contract_id':fields.many2one('hr.contract', 'Contract', required=True),
919 'total': fields.float('Amount', digits_compute=dp.get_precision('Account')),
920 'company_contrib': fields.float('Company Contribution', readonly=True, digits_compute=dp.get_precision('Account')),
925 class hr_payroll_structure(osv.osv):
927 _inherit = 'hr.payroll.structure'
929 'rule_ids':fields.many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id', 'Salary Rules'),
932 hr_payroll_structure()
934 class hr_employee(osv.osv):
939 _inherit = 'hr.employee'
940 _description = 'Employee'
942 def _calculate_total_wage(self, cr, uid, ids, name, args, context):
943 if not ids: return {}
945 current_date = datetime.now().strftime('%Y-%m-%d')
946 for employee in self.browse(cr, uid, ids, context=context):
947 if not employee.contract_ids:
948 res[employee.id] = {'basic': 0.0}
950 cr.execute( 'SELECT SUM(wage) '\
952 'WHERE employee_id = %s '\
953 'AND date_start <= %s '\
954 'AND (date_end > %s OR date_end is NULL)',
955 (employee.id, current_date, current_date))
956 result = dict(cr.dictfetchone())
957 res[employee.id] = {'basic': result['sum']}
961 'slip_ids':fields.one2many('hr.payslip', 'employee_id', 'Payslips', required=False, readonly=True),
962 'total_wage': fields.function(_calculate_total_wage, method=True, type='float', string='Total Basic Salary', digits_compute=dp.get_precision('Account'), help="Sum of all current contract's wage of employee."),
967 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: