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
29 from openerp.osv import fields, osv
30 from openerp import tools
31 from openerp.tools.translate import _
32 import openerp.addons.decimal_precision as dp
34 from openerp.tools.safe_eval import safe_eval as eval
36 class hr_payroll_structure(osv.osv):
38 Salary structure used to defined
44 _name = 'hr.payroll.structure'
45 _description = 'Salary Structure'
47 'name':fields.char('Name', size=256, required=True),
48 'code':fields.char('Reference', size=64, required=True),
49 'company_id':fields.many2one('res.company', 'Company', required=True),
50 'note': fields.text('Description'),
51 'parent_id':fields.many2one('hr.payroll.structure', 'Parent'),
52 'children_ids':fields.one2many('hr.payroll.structure', 'parent_id', 'Children'),
53 'rule_ids':fields.many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id', 'Salary Rules'),
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,
72 (osv.osv._check_recursion, 'Error ! You cannot create a recursive Salary Structure.', ['parent_id'])
75 def copy(self, cr, uid, id, default=None, context=None):
77 Create a new record in hr_payroll_structure model from existing one
78 @param cr: cursor to database
79 @param user: id of current user
80 @param id: list of record ids on which copy method executes
81 @param default: dict type contains the values to be override during copy of object
82 @param context: context arguments, like lang, time zone
84 @return: returns a id of newly created record
89 code=_("%s (copy)") % (self.browse(cr, uid, id, context=context).code),
90 company_id=self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id)
91 return super(hr_payroll_structure, self).copy(cr, uid, id, default, context=context)
93 def get_all_rules(self, cr, uid, structure_ids, context=None):
95 @param structure_ids: list of structure
96 @return: returns a list of tuple (id, sequence) of rules that are maybe to apply
100 for struct in self.browse(cr, uid, structure_ids, context=context):
101 all_rules += self.pool.get('hr.salary.rule')._recursive_search_of_rules(cr, uid, struct.rule_ids, context=context)
104 def _get_parent_structure(self, cr, uid, struct_ids, context=None):
108 for struct in self.browse(cr, uid, struct_ids, context=context):
110 parent.append(struct.parent_id.id)
112 parent = self._get_parent_structure(cr, uid, parent, context)
113 return parent + struct_ids
116 class hr_contract(osv.osv):
118 Employee contract based on the visa, work permits
119 allows to configure different Salary structure
122 _inherit = 'hr.contract'
123 _description = 'Employee Contract'
125 'struct_id': fields.many2one('hr.payroll.structure', 'Salary Structure'),
126 'schedule_pay': fields.selection([
127 ('monthly', 'Monthly'),
128 ('quarterly', 'Quarterly'),
129 ('semi-annually', 'Semi-annually'),
130 ('annually', 'Annually'),
131 ('weekly', 'Weekly'),
132 ('bi-weekly', 'Bi-weekly'),
133 ('bi-monthly', 'Bi-monthly'),
134 ], 'Scheduled Pay', select=True),
138 'schedule_pay': 'monthly',
141 def get_all_structures(self, cr, uid, contract_ids, context=None):
143 @param contract_ids: list of contracts
144 @return: the structures linked to the given contracts, ordered by hierachy (parent=False first, then first level children and so on) and without duplicata
146 structure_ids = [contract.struct_id.id for contract in self.browse(cr, uid, contract_ids, context=context) if contract.struct_id]
147 if not structure_ids:
149 return list(set(self.pool.get('hr.payroll.structure')._get_parent_structure(cr, uid, structure_ids, context=context)))
152 class contrib_register(osv.osv):
154 Contribution Register
157 _name = 'hr.contribution.register'
158 _description = 'Contribution Register'
161 'company_id':fields.many2one('res.company', 'Company'),
162 'partner_id':fields.many2one('res.partner', 'Partner'),
163 'name':fields.char('Name', size=256, required=True, readonly=False),
164 'register_line_ids':fields.one2many('hr.payslip.line', 'register_id', 'Register Line', readonly=True),
165 'note': fields.text('Description'),
168 'company_id': lambda self, cr, uid, context: \
169 self.pool.get('res.users').browse(cr, uid, uid,
170 context=context).company_id.id,
174 class hr_salary_rule_category(osv.osv):
176 HR Salary Rule Category
179 _name = 'hr.salary.rule.category'
180 _description = 'Salary Rule Category'
182 'name':fields.char('Name', size=64, required=True, readonly=False),
183 'code':fields.char('Code', size=64, required=True, readonly=False),
184 'parent_id':fields.many2one('hr.salary.rule.category', 'Parent', help="Linking a salary category to its parent is used only for the reporting purpose."),
185 'children_ids': fields.one2many('hr.salary.rule.category', 'parent_id', 'Children'),
186 'note': fields.text('Description'),
187 'company_id':fields.many2one('res.company', 'Company', required=False),
191 'company_id': lambda self, cr, uid, context: \
192 self.pool.get('res.users').browse(cr, uid, uid,
193 context=context).company_id.id,
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[self._obj].search(cr, user, [(self._fields_id,'in',ids), ('appears_on_payslip', '=', True)], limit=self._limit)
208 for r in obj.pool[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)
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('Payslip Name', 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('Internal Note', 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 for payslip in self.browse(cr, uid, ids, context=context):
335 id_copy = self.copy(cr, uid, payslip.id, {'credit_note': True, 'name': _('Refund: ')+payslip.name}, context=context)
336 self.compute_sheet(cr, uid, [id_copy], context=context)
337 self.signal_hr_verify_sheet(cr, uid, [id_copy])
338 self.signal_process_sheet(cr, uid, [id_copy])
340 form_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_form')
341 form_res = form_id and form_id[1] or False
342 tree_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_tree')
343 tree_res = tree_id and tree_id[1] or False
345 'name':_("Refund Payslip"),
346 'view_mode': 'tree, form',
349 'res_model': 'hr.payslip',
350 'type': 'ir.actions.act_window',
353 'domain': "[('id', 'in', %s)]" % [id_copy],
354 'views': [(tree_res, 'tree'), (form_res, 'form')],
358 def check_done(self, cr, uid, ids, context=None):
361 def unlink(self, cr, uid, ids, context=None):
362 for payslip in self.browse(cr, uid, ids, context=context):
363 if payslip.state not in ['draft','cancel']:
364 raise osv.except_osv(_('Warning!'),_('You cannot delete a payslip which is not draft or cancelled!'))
365 return super(hr_payslip, self).unlink(cr, uid, ids, context)
367 #TODO move this function into hr_contract module, on hr.employee object
368 def get_contract(self, cr, uid, employee, date_from, date_to, context=None):
370 @param employee: browse record of employee
371 @param date_from: date field
372 @param date_to: date field
373 @return: returns the ids of all the contracts for the given employee that need to be considered for the given dates
375 contract_obj = self.pool.get('hr.contract')
377 #a contract is valid if it ends between the given dates
378 clause_1 = ['&',('date_end', '<=', date_to),('date_end','>=', date_from)]
379 #OR if it starts between the given dates
380 clause_2 = ['&',('date_start', '<=', date_to),('date_start','>=', date_from)]
381 #OR if it starts before the date_from and finish after the date_end (or never finish)
382 clause_3 = [('date_start','<=', date_from),'|',('date_end', '=', False),('date_end','>=', date_to)]
383 clause_final = [('employee_id', '=', employee.id),'|','|'] + clause_1 + clause_2 + clause_3
384 contract_ids = contract_obj.search(cr, uid, clause_final, context=context)
387 def compute_sheet(self, cr, uid, ids, context=None):
388 slip_line_pool = self.pool.get('hr.payslip.line')
389 sequence_obj = self.pool.get('ir.sequence')
390 for payslip in self.browse(cr, uid, ids, context=context):
391 number = payslip.number or sequence_obj.get(cr, uid, 'salary.slip')
392 #delete old payslip lines
393 old_slipline_ids = slip_line_pool.search(cr, uid, [('slip_id', '=', payslip.id)], context=context)
396 slip_line_pool.unlink(cr, uid, old_slipline_ids, context=context)
397 if payslip.contract_id:
398 #set the list of contract for which the rules have to be applied
399 contract_ids = [payslip.contract_id.id]
401 #if we don't give the contract, then the rules to apply should be for all current contracts of the employee
402 contract_ids = self.get_contract(cr, uid, payslip.employee_id, payslip.date_from, payslip.date_to, context=context)
403 lines = [(0,0,line) for line in self.pool.get('hr.payslip').get_payslip_lines(cr, uid, contract_ids, payslip.id, context=context)]
404 self.write(cr, uid, [payslip.id], {'line_ids': lines, 'number': number,}, context=context)
407 def get_worked_day_lines(self, cr, uid, contract_ids, date_from, date_to, context=None):
409 @param contract_ids: list of contract id
410 @return: returns a list of dict containing the input that should be applied for the given contract between date_from and date_to
412 def was_on_leave(employee_id, datetime_day, context=None):
414 day = datetime_day.strftime("%Y-%m-%d")
415 holiday_ids = self.pool.get('hr.holidays').search(cr, uid, [('state','=','validate'),('employee_id','=',employee_id),('type','=','remove'),('date_from','<=',day),('date_to','>=',day)])
417 res = self.pool.get('hr.holidays').browse(cr, uid, holiday_ids, context=context)[0].holiday_status_id.name
421 for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
422 if not contract.working_hours:
423 #fill only if the contract as a working schedule linked
426 'name': _("Normal Working Days paid at 100%"),
429 'number_of_days': 0.0,
430 'number_of_hours': 0.0,
431 'contract_id': contract.id,
434 day_from = datetime.strptime(date_from,"%Y-%m-%d")
435 day_to = datetime.strptime(date_to,"%Y-%m-%d")
436 nb_of_days = (day_to - day_from).days + 1
437 for day in range(0, nb_of_days):
438 working_hours_on_day = self.pool.get('resource.calendar').working_hours_on_day(cr, uid, contract.working_hours, day_from + timedelta(days=day), context)
439 if working_hours_on_day:
440 #the employee had to work
441 leave_type = was_on_leave(contract.employee_id.id, day_from + timedelta(days=day), context=context)
443 #if he was on leave, fill the leaves dict
444 if leave_type in leaves:
445 leaves[leave_type]['number_of_days'] += 1.0
446 leaves[leave_type]['number_of_hours'] += working_hours_on_day
448 leaves[leave_type] = {
452 'number_of_days': 1.0,
453 'number_of_hours': working_hours_on_day,
454 'contract_id': contract.id,
457 #add the input vals to tmp (increment if existing)
458 attendances['number_of_days'] += 1.0
459 attendances['number_of_hours'] += working_hours_on_day
460 leaves = [value for key,value in leaves.items()]
461 res += [attendances] + leaves
464 def get_inputs(self, cr, uid, contract_ids, date_from, date_to, context=None):
466 contract_obj = self.pool.get('hr.contract')
467 rule_obj = self.pool.get('hr.salary.rule')
469 structure_ids = contract_obj.get_all_structures(cr, uid, contract_ids, context=context)
470 rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
471 sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
473 for contract in contract_obj.browse(cr, uid, contract_ids, context=context):
474 for rule in rule_obj.browse(cr, uid, sorted_rule_ids, context=context):
476 for input in rule.input_ids:
480 'contract_id': contract.id,
485 def get_payslip_lines(self, cr, uid, contract_ids, payslip_id, context):
486 def _sum_salary_rule_category(localdict, category, amount):
487 if category.parent_id:
488 localdict = _sum_salary_rule_category(localdict, category.parent_id, amount)
489 localdict['categories'].dict[category.code] = category.code in localdict['categories'].dict and localdict['categories'].dict[category.code] + amount or amount
492 class BrowsableObject(object):
493 def __init__(self, pool, cr, uid, employee_id, dict):
497 self.employee_id = employee_id
500 def __getattr__(self, attr):
501 return attr in self.dict and self.dict.__getitem__(attr) or 0.0
503 class InputLine(BrowsableObject):
504 """a class that will be used into the python code, mainly for usability purposes"""
505 def sum(self, code, from_date, to_date=None):
507 to_date = datetime.now().strftime('%Y-%m-%d')
509 self.cr.execute("SELECT sum(amount) as sum\
510 FROM hr_payslip as hp, hr_payslip_input as pi \
511 WHERE hp.employee_id = %s AND hp.state = 'done' \
512 AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s",
513 (self.employee_id, from_date, to_date, code))
514 res = self.cr.fetchone()[0]
517 class WorkedDays(BrowsableObject):
518 """a class that will be used into the python code, mainly for usability purposes"""
519 def _sum(self, code, from_date, to_date=None):
521 to_date = datetime.now().strftime('%Y-%m-%d')
523 self.cr.execute("SELECT sum(number_of_days) as number_of_days, sum(number_of_hours) as number_of_hours\
524 FROM hr_payslip as hp, hr_payslip_worked_days as pi \
525 WHERE hp.employee_id = %s AND hp.state = 'done'\
526 AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s",
527 (self.employee_id, from_date, to_date, code))
528 return self.cr.fetchone()
530 def sum(self, code, from_date, to_date=None):
531 res = self._sum(code, from_date, to_date)
532 return res and res[0] or 0.0
534 def sum_hours(self, code, from_date, to_date=None):
535 res = self._sum(code, from_date, to_date)
536 return res and res[1] or 0.0
538 class Payslips(BrowsableObject):
539 """a class that will be used into the python code, mainly for usability purposes"""
541 def sum(self, code, from_date, to_date=None):
543 to_date = datetime.now().strftime('%Y-%m-%d')
544 self.cr.execute("SELECT sum(case when hp.credit_note = False then (pl.total) else (-pl.total) end)\
545 FROM hr_payslip as hp, hr_payslip_line as pl \
546 WHERE hp.employee_id = %s AND hp.state = 'done' \
547 AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id AND pl.code = %s",
548 (self.employee_id, from_date, to_date, code))
549 res = self.cr.fetchone()
550 return res and res[0] or 0.0
552 #we keep a dict with the result because a value can be overwritten by another rule with the same code
557 payslip_obj = self.pool.get('hr.payslip')
558 inputs_obj = self.pool.get('hr.payslip.worked_days')
559 obj_rule = self.pool.get('hr.salary.rule')
560 payslip = payslip_obj.browse(cr, uid, payslip_id, context=context)
562 for worked_days_line in payslip.worked_days_line_ids:
563 worked_days[worked_days_line.code] = worked_days_line
565 for input_line in payslip.input_line_ids:
566 inputs[input_line.code] = input_line
568 categories_obj = BrowsableObject(self.pool, cr, uid, payslip.employee_id.id, categories_dict)
569 input_obj = InputLine(self.pool, cr, uid, payslip.employee_id.id, inputs)
570 worked_days_obj = WorkedDays(self.pool, cr, uid, payslip.employee_id.id, worked_days)
571 payslip_obj = Payslips(self.pool, cr, uid, payslip.employee_id.id, payslip)
572 rules_obj = BrowsableObject(self.pool, cr, uid, payslip.employee_id.id, rules)
574 localdict = {'categories': categories_obj, 'rules': rules_obj, 'payslip': payslip_obj, 'worked_days': worked_days_obj, 'inputs': input_obj}
575 #get the ids of the structures on the contracts and their parent id as well
576 structure_ids = self.pool.get('hr.contract').get_all_structures(cr, uid, contract_ids, context=context)
577 #get the rules of the structure and thier children
578 rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
579 #run the rules by sequence
580 sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
582 for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
583 employee = contract.employee_id
584 localdict.update({'employee': employee, 'contract': contract})
585 for rule in obj_rule.browse(cr, uid, sorted_rule_ids, context=context):
586 key = rule.code + '-' + str(contract.id)
587 localdict['result'] = None
588 localdict['result_qty'] = 1.0
589 #check if the rule can be applied
590 if obj_rule.satisfy_condition(cr, uid, rule.id, localdict, context=context) and rule.id not in blacklist:
591 #compute the amount of the rule
592 amount, qty, rate = obj_rule.compute_rule(cr, uid, rule.id, localdict, context=context)
593 #check if there is already a rule computed with that code
594 previous_amount = rule.code in localdict and localdict[rule.code] or 0.0
595 #set/overwrite the amount computed for this rule in the localdict
596 tot_rule = amount * qty * rate / 100.0
597 localdict[rule.code] = tot_rule
598 rules[rule.code] = rule
599 #sum the amount for its salary category
600 localdict = _sum_salary_rule_category(localdict, rule.category_id, tot_rule - previous_amount)
601 #create/overwrite the rule in the temporary results
603 'salary_rule_id': rule.id,
604 'contract_id': contract.id,
607 'category_id': rule.category_id.id,
608 'sequence': rule.sequence,
609 'appears_on_payslip': rule.appears_on_payslip,
610 'condition_select': rule.condition_select,
611 'condition_python': rule.condition_python,
612 'condition_range': rule.condition_range,
613 'condition_range_min': rule.condition_range_min,
614 'condition_range_max': rule.condition_range_max,
615 'amount_select': rule.amount_select,
616 'amount_fix': rule.amount_fix,
617 'amount_python_compute': rule.amount_python_compute,
618 'amount_percentage': rule.amount_percentage,
619 'amount_percentage_base': rule.amount_percentage_base,
620 'register_id': rule.register_id.id,
622 'employee_id': contract.employee_id.id,
627 #blacklist this rule and its children
628 blacklist += [id for id, seq in self.pool.get('hr.salary.rule')._recursive_search_of_rules(cr, uid, [rule], context=context)]
630 result = [value for code, value in result_dict.items()]
633 def onchange_employee_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
634 empolyee_obj = self.pool.get('hr.employee')
635 contract_obj = self.pool.get('hr.contract')
636 worked_days_obj = self.pool.get('hr.payslip.worked_days')
637 input_obj = self.pool.get('hr.payslip.input')
641 #delete old worked days lines
642 old_worked_days_ids = ids and worked_days_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
643 if old_worked_days_ids:
644 worked_days_obj.unlink(cr, uid, old_worked_days_ids, context=context)
646 #delete old input lines
647 old_input_ids = ids and input_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
649 input_obj.unlink(cr, uid, old_input_ids, context=context)
655 'input_line_ids': [],
656 'worked_days_line_ids': [],
657 #'details_by_salary_head':[], TODO put me back
659 'contract_id': False,
663 if (not employee_id) or (not date_from) or (not date_to):
665 ttyme = datetime.fromtimestamp(time.mktime(time.strptime(date_from, "%Y-%m-%d")))
666 employee_id = empolyee_obj.browse(cr, uid, employee_id, context=context)
667 res['value'].update({
668 'name': _('Salary Slip of %s for %s') % (employee_id.name, tools.ustr(ttyme.strftime('%B-%Y'))),
669 'company_id': employee_id.company_id.id
672 if not context.get('contract', False):
673 #fill with the first contract of the employee
674 contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
677 #set the list of contract for which the input have to be filled
678 contract_ids = [contract_id]
680 #if we don't give the contract, then the input to fill should be for all current contracts of the employee
681 contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
685 contract_record = contract_obj.browse(cr, uid, contract_ids[0], context=context)
686 res['value'].update({
687 'contract_id': contract_record and contract_record.id or False
689 struct_record = contract_record and contract_record.struct_id or False
690 if not struct_record:
692 res['value'].update({
693 'struct_id': struct_record.id,
695 #computation of the salary input
696 worked_days_line_ids = self.get_worked_day_lines(cr, uid, contract_ids, date_from, date_to, context=context)
697 input_line_ids = self.get_inputs(cr, uid, contract_ids, date_from, date_to, context=context)
698 res['value'].update({
699 'worked_days_line_ids': worked_days_line_ids,
700 'input_line_ids': input_line_ids,
704 def onchange_contract_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
705 #TODO it seems to be the mess in the onchanges, we should have onchange_employee => onchange_contract => doing all the things
713 context.update({'contract': True})
715 res['value'].update({'struct_id': False})
716 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)
719 class hr_payslip_worked_days(osv.osv):
724 _name = 'hr.payslip.worked_days'
725 _description = 'Payslip Worked Days'
727 'name': fields.char('Description', size=256, required=True),
728 'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade', select=True),
729 'sequence': fields.integer('Sequence', required=True, select=True),
730 'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
731 'number_of_days': fields.float('Number of Days'),
732 'number_of_hours': fields.float('Number of Hours'),
733 'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
735 _order = 'payslip_id, sequence'
740 class hr_payslip_input(osv.osv):
745 _name = 'hr.payslip.input'
746 _description = 'Payslip Input'
748 'name': fields.char('Description', size=256, required=True),
749 'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade', select=True),
750 'sequence': fields.integer('Sequence', required=True, select=True),
751 'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
752 '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."),
753 'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
755 _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))
899 class hr_rule_input(osv.osv):
904 _name = 'hr.rule.input'
905 _description = 'Salary Rule Input'
907 'name': fields.char('Description', size=256, required=True),
908 'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
909 'input_id': fields.many2one('hr.salary.rule', 'Salary Rule Input', required=True)
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_employee(osv.osv):
953 _inherit = 'hr.employee'
954 _description = 'Employee'
956 def _calculate_total_wage(self, cr, uid, ids, name, args, context):
957 if not ids: return {}
959 current_date = datetime.now().strftime('%Y-%m-%d')
960 for employee in self.browse(cr, uid, ids, context=context):
961 if not employee.contract_ids:
962 res[employee.id] = {'basic': 0.0}
964 cr.execute( 'SELECT SUM(wage) '\
966 'WHERE employee_id = %s '\
967 'AND date_start <= %s '\
968 'AND (date_end > %s OR date_end is NULL)',
969 (employee.id, current_date, current_date))
970 result = dict(cr.dictfetchone())
971 res[employee.id] = {'basic': result['sum']}
975 'slip_ids':fields.one2many('hr.payslip', 'employee_id', 'Payslips', required=False, readonly=True),
976 '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."),
980 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: