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
147 structure_ids = [contract.struct_id.id for contract in self.browse(cr, uid, contract_ids, context=context)]
148 return list(set(self.pool.get('hr.payroll.structure')._get_parent_structure(cr, uid, structure_ids, context=context)))
151 class contrib_register(osv.osv):
153 Contribution Register
156 _name = 'hr.contribution.register'
157 _description = 'Contribution Register'
160 'company_id':fields.many2one('res.company', 'Company'),
161 'partner_id':fields.many2one('res.partner', 'Partner'),
162 'name':fields.char('Name', size=256, required=True, readonly=False),
163 'register_line_ids':fields.one2many('hr.payslip.line', 'register_id', 'Register Line', readonly=True),
164 'note': fields.text('Description'),
167 'company_id': lambda self, cr, uid, context: \
168 self.pool.get('res.users').browse(cr, uid, uid,
169 context=context).company_id.id,
173 class hr_salary_rule_category(osv.osv):
175 HR Salary Rule Category
178 _name = 'hr.salary.rule.category'
179 _description = 'Salary Rule Category'
181 'name':fields.char('Name', size=64, required=True, readonly=False),
182 'code':fields.char('Code', size=64, required=True, readonly=False),
183 'parent_id':fields.many2one('hr.salary.rule.category', 'Parent', help="Linking a salary category to its parent is used only for the reporting purpose."),
184 'children_ids': fields.one2many('hr.salary.rule.category', 'parent_id', 'Children'),
185 'note': fields.text('Description'),
186 'company_id':fields.many2one('res.company', 'Company', required=False),
190 'company_id': lambda self, cr, uid, context: \
191 self.pool.get('res.users').browse(cr, uid, uid,
192 context=context).company_id.id,
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[self._obj].search(cr, user, [(self._fields_id,'in',ids), ('appears_on_payslip', '=', True)], limit=self._limit)
207 for r in obj.pool[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)
239 class hr_payslip(osv.osv):
245 _description = 'Pay Slip'
247 def _get_lines_salary_rule_category(self, cr, uid, ids, field_names, arg=None, context=None):
249 if not ids: return result
251 result.setdefault(id, [])
252 cr.execute('''SELECT pl.slip_id, pl.id FROM hr_payslip_line AS pl \
253 LEFT JOIN hr_salary_rule_category AS sh on (pl.category_id = sh.id) \
254 WHERE pl.slip_id in %s \
255 GROUP BY pl.slip_id, pl.sequence, pl.id ORDER BY pl.sequence''',(tuple(ids),))
258 result[r[0]].append(r[1])
262 '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'),
263 'name': fields.char('Description', size=64, required=False, readonly=True, states={'draft': [('readonly', False)]}),
264 'number': fields.char('Reference', size=64, required=False, readonly=True, states={'draft': [('readonly', False)]}),
265 'employee_id': fields.many2one('hr.employee', 'Employee', required=True, readonly=True, states={'draft': [('readonly', False)]}),
266 'date_from': fields.date('Date From', readonly=True, states={'draft': [('readonly', False)]}, required=True),
267 'date_to': fields.date('Date To', readonly=True, states={'draft': [('readonly', False)]}, required=True),
268 'state': fields.selection([
270 ('verify', 'Waiting'),
272 ('cancel', 'Rejected'),
273 ], 'Status', select=True, readonly=True,
274 help='* When the payslip is created the status is \'Draft\'.\
275 \n* If the payslip is under verification, the status is \'Waiting\'. \
276 \n* If the payslip is confirmed then status is set to \'Done\'.\
277 \n* When user cancel payslip the status is \'Rejected\'.'),
278 # 'line_ids': fields.one2many('hr.payslip.line', 'slip_id', 'Payslip Line', required=False, readonly=True, states={'draft': [('readonly', False)]}),
279 'line_ids': one2many_mod2('hr.payslip.line', 'slip_id', 'Payslip Lines', readonly=True, states={'draft':[('readonly',False)]}),
280 'company_id': fields.many2one('res.company', 'Company', required=False, readonly=True, states={'draft': [('readonly', False)]}),
281 'worked_days_line_ids': fields.one2many('hr.payslip.worked_days', 'payslip_id', 'Payslip Worked Days', required=False, readonly=True, states={'draft': [('readonly', False)]}),
282 'input_line_ids': fields.one2many('hr.payslip.input', 'payslip_id', 'Payslip Inputs', required=False, readonly=True, states={'draft': [('readonly', False)]}),
283 'paid': fields.boolean('Made Payment Order ? ', required=False, readonly=True, states={'draft': [('readonly', False)]}),
284 'note': fields.text('Description', readonly=True, states={'draft':[('readonly',False)]}),
285 'contract_id': fields.many2one('hr.contract', 'Contract', required=False, readonly=True, states={'draft': [('readonly', False)]}),
286 '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'),
287 'credit_note': fields.boolean('Credit Note', help="Indicates this payslip has a refund of another", readonly=True, states={'draft': [('readonly', False)]}),
288 'payslip_run_id': fields.many2one('hr.payslip.run', 'Payslip Batches', readonly=True, states={'draft': [('readonly', False)]}),
291 'date_from': lambda *a: time.strftime('%Y-%m-01'),
292 'date_to': lambda *a: str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10],
294 'credit_note': False,
295 'company_id': lambda self, cr, uid, context: \
296 self.pool.get('res.users').browse(cr, uid, uid,
297 context=context).company_id.id,
300 def _check_dates(self, cr, uid, ids, context=None):
301 for payslip in self.browse(cr, uid, ids, context=context):
302 if payslip.date_from > payslip.date_to:
306 _constraints = [(_check_dates, "Payslip 'Date From' must be before 'Date To'.", ['date_from', 'date_to'])]
308 def copy(self, cr, uid, id, default=None, context=None):
311 company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
314 'company_id': company_id,
316 'payslip_run_id': False,
319 return super(hr_payslip, self).copy(cr, uid, id, default, context=context)
321 def cancel_sheet(self, cr, uid, ids, context=None):
322 return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
324 def process_sheet(self, cr, uid, ids, context=None):
325 return self.write(cr, uid, ids, {'paid': True, 'state': 'done'}, context=context)
327 def hr_verify_sheet(self, cr, uid, ids, context=None):
328 self.compute_sheet(cr, uid, ids, context)
329 return self.write(cr, uid, ids, {'state': 'verify'}, context=context)
331 def refund_sheet(self, cr, uid, ids, context=None):
332 mod_obj = self.pool.get('ir.model.data')
333 for payslip in self.browse(cr, uid, ids, context=context):
334 id_copy = self.copy(cr, uid, payslip.id, {'credit_note': True, 'name': _('Refund: ')+payslip.name}, context=context)
335 self.compute_sheet(cr, uid, [id_copy], context=context)
336 self.signal_hr_verify_sheet(cr, uid, [id_copy])
337 self.signal_process_sheet(cr, uid, [id_copy])
339 form_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_form')
340 form_res = form_id and form_id[1] or False
341 tree_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_tree')
342 tree_res = tree_id and tree_id[1] or False
344 'name':_("Refund Payslip"),
345 'view_mode': 'tree, form',
348 'res_model': 'hr.payslip',
349 'type': 'ir.actions.act_window',
352 'domain': "[('id', 'in', %s)]" % [id_copy],
353 'views': [(tree_res, 'tree'), (form_res, 'form')],
357 def check_done(self, cr, uid, ids, context=None):
360 def unlink(self, cr, uid, ids, context=None):
361 for payslip in self.browse(cr, uid, ids, context=context):
362 if payslip.state not in ['draft','cancel']:
363 raise osv.except_osv(_('Warning!'),_('You cannot delete a payslip which is not draft or cancelled!'))
364 return super(hr_payslip, self).unlink(cr, uid, ids, context)
366 #TODO move this function into hr_contract module, on hr.employee object
367 def get_contract(self, cr, uid, employee, date_from, date_to, context=None):
369 @param employee: browse record of employee
370 @param date_from: date field
371 @param date_to: date field
372 @return: returns the ids of all the contracts for the given employee that need to be considered for the given dates
374 contract_obj = self.pool.get('hr.contract')
376 #a contract is valid if it ends between the given dates
377 clause_1 = ['&',('date_end', '<=', date_to),('date_end','>=', date_from)]
378 #OR if it starts between the given dates
379 clause_2 = ['&',('date_start', '<=', date_to),('date_start','>=', date_from)]
380 #OR if it starts before the date_from and finish after the date_end (or never finish)
381 clause_3 = [('date_start','<=', date_from),'|',('date_end', '=', False),('date_end','>=', date_to)]
382 clause_final = [('employee_id', '=', employee.id),'|','|'] + clause_1 + clause_2 + clause_3
383 contract_ids = contract_obj.search(cr, uid, clause_final, context=context)
386 def compute_sheet(self, cr, uid, ids, context=None):
387 slip_line_pool = self.pool.get('hr.payslip.line')
388 sequence_obj = self.pool.get('ir.sequence')
389 for payslip in self.browse(cr, uid, ids, context=context):
390 number = payslip.number or sequence_obj.get(cr, uid, 'salary.slip')
391 #delete old payslip lines
392 old_slipline_ids = slip_line_pool.search(cr, uid, [('slip_id', '=', payslip.id)], context=context)
395 slip_line_pool.unlink(cr, uid, old_slipline_ids, context=context)
396 if payslip.contract_id:
397 #set the list of contract for which the rules have to be applied
398 contract_ids = [payslip.contract_id.id]
400 #if we don't give the contract, then the rules to apply should be for all current contracts of the employee
401 contract_ids = self.get_contract(cr, uid, payslip.employee_id, payslip.date_from, payslip.date_to, context=context)
402 lines = [(0,0,line) for line in self.pool.get('hr.payslip').get_payslip_lines(cr, uid, contract_ids, payslip.id, context=context)]
403 self.write(cr, uid, [payslip.id], {'line_ids': lines, 'number': number,}, context=context)
406 def get_worked_day_lines(self, cr, uid, contract_ids, date_from, date_to, context=None):
408 @param contract_ids: list of contract id
409 @return: returns a list of dict containing the input that should be applied for the given contract between date_from and date_to
411 def was_on_leave(employee_id, datetime_day, context=None):
413 day = datetime_day.strftime("%Y-%m-%d")
414 holiday_ids = self.pool.get('hr.holidays').search(cr, uid, [('state','=','validate'),('employee_id','=',employee_id),('type','=','remove'),('date_from','<=',day),('date_to','>=',day)])
416 res = self.pool.get('hr.holidays').browse(cr, uid, holiday_ids, context=context)[0].holiday_status_id.name
420 for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
421 if not contract.working_hours:
422 #fill only if the contract as a working schedule linked
425 'name': _("Normal Working Days paid at 100%"),
428 'number_of_days': 0.0,
429 'number_of_hours': 0.0,
430 'contract_id': contract.id,
433 day_from = datetime.strptime(date_from,"%Y-%m-%d")
434 day_to = datetime.strptime(date_to,"%Y-%m-%d")
435 nb_of_days = (day_to - day_from).days + 1
436 for day in range(0, nb_of_days):
437 working_hours_on_day = self.pool.get('resource.calendar').working_hours_on_day(cr, uid, contract.working_hours, day_from + timedelta(days=day), context)
438 if working_hours_on_day:
439 #the employee had to work
440 leave_type = was_on_leave(contract.employee_id.id, day_from + timedelta(days=day), context=context)
442 #if he was on leave, fill the leaves dict
443 if leave_type in leaves:
444 leaves[leave_type]['number_of_days'] += 1.0
445 leaves[leave_type]['number_of_hours'] += working_hours_on_day
447 leaves[leave_type] = {
451 'number_of_days': 1.0,
452 'number_of_hours': working_hours_on_day,
453 'contract_id': contract.id,
456 #add the input vals to tmp (increment if existing)
457 attendances['number_of_days'] += 1.0
458 attendances['number_of_hours'] += working_hours_on_day
459 leaves = [value for key,value in leaves.items()]
460 res += [attendances] + leaves
463 def get_inputs(self, cr, uid, contract_ids, date_from, date_to, context=None):
465 contract_obj = self.pool.get('hr.contract')
466 rule_obj = self.pool.get('hr.salary.rule')
468 structure_ids = contract_obj.get_all_structures(cr, uid, contract_ids, context=context)
469 rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
470 sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
472 for contract in contract_obj.browse(cr, uid, contract_ids, context=context):
473 for rule in rule_obj.browse(cr, uid, sorted_rule_ids, context=context):
475 for input in rule.input_ids:
479 'contract_id': contract.id,
484 def get_payslip_lines(self, cr, uid, contract_ids, payslip_id, context):
485 def _sum_salary_rule_category(localdict, category, amount):
486 if category.parent_id:
487 localdict = _sum_salary_rule_category(localdict, category.parent_id, amount)
488 localdict['categories'].dict[category.code] = category.code in localdict['categories'].dict and localdict['categories'].dict[category.code] + amount or amount
491 class BrowsableObject(object):
492 def __init__(self, pool, cr, uid, employee_id, dict):
496 self.employee_id = employee_id
499 def __getattr__(self, attr):
500 return attr in self.dict and self.dict.__getitem__(attr) or 0.0
502 class InputLine(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(amount) as sum\
509 FROM hr_payslip as hp, hr_payslip_input as pi \
510 WHERE hp.employee_id = %s AND hp.state = '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 res = self.cr.fetchone()[0]
516 class WorkedDays(BrowsableObject):
517 """a class that will be used into the python code, mainly for usability purposes"""
518 def _sum(self, code, from_date, to_date=None):
520 to_date = datetime.now().strftime('%Y-%m-%d')
522 self.cr.execute("SELECT sum(number_of_days) as number_of_days, sum(number_of_hours) as number_of_hours\
523 FROM hr_payslip as hp, hr_payslip_worked_days as pi \
524 WHERE hp.employee_id = %s AND hp.state = 'done'\
525 AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s",
526 (self.employee_id, from_date, to_date, code))
527 return self.cr.fetchone()
529 def sum(self, code, from_date, to_date=None):
530 res = self._sum(code, from_date, to_date)
531 return res and res[0] or 0.0
533 def sum_hours(self, code, from_date, to_date=None):
534 res = self._sum(code, from_date, to_date)
535 return res and res[1] or 0.0
537 class Payslips(BrowsableObject):
538 """a class that will be used into the python code, mainly for usability purposes"""
540 def sum(self, code, from_date, to_date=None):
542 to_date = datetime.now().strftime('%Y-%m-%d')
543 self.cr.execute("SELECT sum(case when hp.credit_note = False then (pl.total) else (-pl.total) end)\
544 FROM hr_payslip as hp, hr_payslip_line as pl \
545 WHERE hp.employee_id = %s AND hp.state = 'done' \
546 AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id AND pl.code = %s",
547 (self.employee_id, from_date, to_date, code))
548 res = self.cr.fetchone()
549 return res and res[0] or 0.0
551 #we keep a dict with the result because a value can be overwritten by another rule with the same code
556 payslip_obj = self.pool.get('hr.payslip')
557 inputs_obj = self.pool.get('hr.payslip.worked_days')
558 obj_rule = self.pool.get('hr.salary.rule')
559 payslip = payslip_obj.browse(cr, uid, payslip_id, context=context)
561 for worked_days_line in payslip.worked_days_line_ids:
562 worked_days[worked_days_line.code] = worked_days_line
564 for input_line in payslip.input_line_ids:
565 inputs[input_line.code] = input_line
567 categories_obj = BrowsableObject(self.pool, cr, uid, payslip.employee_id.id, categories_dict)
568 input_obj = InputLine(self.pool, cr, uid, payslip.employee_id.id, inputs)
569 worked_days_obj = WorkedDays(self.pool, cr, uid, payslip.employee_id.id, worked_days)
570 payslip_obj = Payslips(self.pool, cr, uid, payslip.employee_id.id, payslip)
571 rules_obj = BrowsableObject(self.pool, cr, uid, payslip.employee_id.id, rules)
573 localdict = {'categories': categories_obj, 'rules': rules_obj, 'payslip': payslip_obj, 'worked_days': worked_days_obj, 'inputs': input_obj}
574 #get the ids of the structures on the contracts and their parent id as well
575 structure_ids = self.pool.get('hr.contract').get_all_structures(cr, uid, contract_ids, context=context)
576 #get the rules of the structure and thier children
577 rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
578 #run the rules by sequence
579 sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
581 for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
582 employee = contract.employee_id
583 localdict.update({'employee': employee, 'contract': contract})
584 for rule in obj_rule.browse(cr, uid, sorted_rule_ids, context=context):
585 key = rule.code + '-' + str(contract.id)
586 localdict['result'] = None
587 localdict['result_qty'] = 1.0
588 #check if the rule can be applied
589 if obj_rule.satisfy_condition(cr, uid, rule.id, localdict, context=context) and rule.id not in blacklist:
590 #compute the amount of the rule
591 amount, qty, rate = obj_rule.compute_rule(cr, uid, rule.id, localdict, context=context)
592 #check if there is already a rule computed with that code
593 previous_amount = rule.code in localdict and localdict[rule.code] or 0.0
594 #set/overwrite the amount computed for this rule in the localdict
595 tot_rule = amount * qty * rate / 100.0
596 localdict[rule.code] = tot_rule
597 rules[rule.code] = rule
598 #sum the amount for its salary category
599 localdict = _sum_salary_rule_category(localdict, rule.category_id, tot_rule - previous_amount)
600 #create/overwrite the rule in the temporary results
602 'salary_rule_id': rule.id,
603 'contract_id': contract.id,
606 'category_id': rule.category_id.id,
607 'sequence': rule.sequence,
608 'appears_on_payslip': rule.appears_on_payslip,
609 'condition_select': rule.condition_select,
610 'condition_python': rule.condition_python,
611 'condition_range': rule.condition_range,
612 'condition_range_min': rule.condition_range_min,
613 'condition_range_max': rule.condition_range_max,
614 'amount_select': rule.amount_select,
615 'amount_fix': rule.amount_fix,
616 'amount_python_compute': rule.amount_python_compute,
617 'amount_percentage': rule.amount_percentage,
618 'amount_percentage_base': rule.amount_percentage_base,
619 'register_id': rule.register_id.id,
621 'employee_id': contract.employee_id.id,
626 #blacklist this rule and its children
627 blacklist += [id for id, seq in self.pool.get('hr.salary.rule')._recursive_search_of_rules(cr, uid, [rule], context=context)]
629 result = [value for code, value in result_dict.items()]
632 def onchange_employee_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
633 empolyee_obj = self.pool.get('hr.employee')
634 contract_obj = self.pool.get('hr.contract')
635 worked_days_obj = self.pool.get('hr.payslip.worked_days')
636 input_obj = self.pool.get('hr.payslip.input')
640 #delete old worked days lines
641 old_worked_days_ids = ids and worked_days_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
642 if old_worked_days_ids:
643 worked_days_obj.unlink(cr, uid, old_worked_days_ids, context=context)
645 #delete old input lines
646 old_input_ids = ids and input_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
648 input_obj.unlink(cr, uid, old_input_ids, context=context)
654 'input_line_ids': [],
655 'worked_days_line_ids': [],
656 #'details_by_salary_head':[], TODO put me back
658 'contract_id': False,
662 if (not employee_id) or (not date_from) or (not date_to):
664 ttyme = datetime.fromtimestamp(time.mktime(time.strptime(date_from, "%Y-%m-%d")))
665 employee_id = empolyee_obj.browse(cr, uid, employee_id, context=context)
666 res['value'].update({
667 'name': _('Salary Slip of %s for %s') % (employee_id.name, tools.ustr(ttyme.strftime('%B-%Y'))),
668 'company_id': employee_id.company_id.id
671 if not context.get('contract', False):
672 #fill with the first contract of the employee
673 contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
676 #set the list of contract for which the input have to be filled
677 contract_ids = [contract_id]
679 #if we don't give the contract, then the input to fill should be for all current contracts of the employee
680 contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
684 contract_record = contract_obj.browse(cr, uid, contract_ids[0], context=context)
685 res['value'].update({
686 'contract_id': contract_record and contract_record.id or False
688 struct_record = contract_record and contract_record.struct_id or False
689 if not struct_record:
691 res['value'].update({
692 'struct_id': struct_record.id,
694 #computation of the salary input
695 worked_days_line_ids = self.get_worked_day_lines(cr, uid, contract_ids, date_from, date_to, context=context)
696 input_line_ids = self.get_inputs(cr, uid, contract_ids, date_from, date_to, context=context)
697 res['value'].update({
698 'worked_days_line_ids': worked_days_line_ids,
699 'input_line_ids': input_line_ids,
703 def onchange_contract_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
704 #TODO it seems to be the mess in the onchanges, we should have onchange_employee => onchange_contract => doing all the things
712 context.update({'contract': True})
714 res['value'].update({'struct_id': False})
715 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)
718 class hr_payslip_worked_days(osv.osv):
723 _name = 'hr.payslip.worked_days'
724 _description = 'Payslip Worked Days'
726 'name': fields.char('Description', size=256, required=True),
727 'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade', select=True),
728 'sequence': fields.integer('Sequence', required=True, select=True),
729 'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
730 'number_of_days': fields.float('Number of Days'),
731 'number_of_hours': fields.float('Number of Hours'),
732 'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
734 _order = 'payslip_id, sequence'
739 class hr_payslip_input(osv.osv):
744 _name = 'hr.payslip.input'
745 _description = 'Payslip Input'
747 'name': fields.char('Description', size=256, required=True),
748 'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade', select=True),
749 'sequence': fields.integer('Sequence', required=True, select=True),
750 'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
751 'amount': fields.float('Amount', help="It is used in computation. For e.g. A rule for sales having 1% commission of basic salary for per product can defined in expression like result = inputs.SALEURO.amount * contract.wage*0.01."),
752 'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
754 _order = 'payslip_id, sequence'
761 class hr_salary_rule(osv.osv):
763 _name = 'hr.salary.rule'
765 'name':fields.char('Name', size=256, required=True, readonly=False),
766 'code':fields.char('Code', size=64, required=True, help="The code of salary rules can be used as reference in computation of other rules. In that case, it is case sensitive."),
767 'sequence': fields.integer('Sequence', required=True, help='Use to arrange calculation sequence', select=True),
768 'quantity': fields.char('Quantity', size=256, help="It is used in computation for percentage and fixed amount.For e.g. A rule for Meal Voucher having fixed amount of 1€ per worked day can have its quantity defined in expression like worked_days.WORK100.number_of_days."),
769 'category_id':fields.many2one('hr.salary.rule.category', 'Category', required=True),
770 'active':fields.boolean('Active', help="If the active field is set to false, it will allow you to hide the salary rule without removing it."),
771 'appears_on_payslip': fields.boolean('Appears on Payslip', help="Used to display the salary rule on payslip."),
772 'parent_rule_id':fields.many2one('hr.salary.rule', 'Parent Salary Rule', select=True),
773 'company_id':fields.many2one('res.company', 'Company', required=False),
774 'condition_select': fields.selection([('none', 'Always True'),('range', 'Range'), ('python', 'Python Expression')], "Condition Based on", required=True),
775 'condition_range':fields.char('Range Based on',size=1024, readonly=False, help='This will be used to compute the % fields values; in general it is on basic, but you can also use categories code fields in lowercase as a variable names (hra, ma, lta, etc.) and the variable basic.'),
776 'condition_python':fields.text('Python Condition', required=True, readonly=False, help='Applied this rule for calculation if condition is true. You can specify condition like basic > 1000.'),
777 'condition_range_min': fields.float('Minimum Range', required=False, help="The minimum amount, applied for this rule."),
778 'condition_range_max': fields.float('Maximum Range', required=False, help="The maximum amount, applied for this rule."),
779 'amount_select':fields.selection([
780 ('percentage','Percentage (%)'),
781 ('fix','Fixed Amount'),
782 ('code','Python Code'),
783 ],'Amount Type', select=True, required=True, help="The computation method for the rule amount."),
784 'amount_fix': fields.float('Fixed Amount', digits_compute=dp.get_precision('Payroll'),),
785 'amount_percentage': fields.float('Percentage (%)', digits_compute=dp.get_precision('Payroll Rate'), help='For example, enter 50.0 to apply a percentage of 50%'),
786 'amount_python_compute':fields.text('Python Code'),
787 'amount_percentage_base':fields.char('Percentage based on',size=1024, required=False, readonly=False, help='result will be affected to a variable'),
788 'child_ids':fields.one2many('hr.salary.rule', 'parent_rule_id', 'Child Salary Rule'),
789 'register_id':fields.many2one('hr.contribution.register', 'Contribution Register', help="Eventual third party involved in the salary payment of the employees."),
790 'input_ids': fields.one2many('hr.rule.input', 'input_id', 'Inputs'),
791 'note':fields.text('Description'),
794 'amount_python_compute': '''
795 # Available variables:
796 #----------------------
797 # payslip: object containing the payslips
798 # employee: hr.employee object
799 # contract: hr.contract object
800 # rules: object containing the rules code (previously computed)
801 # categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category).
802 # worked_days: object containing the computed worked days.
803 # inputs: object containing the computed inputs.
805 # Note: returned value have to be set in the variable 'result'
807 result = contract.wage * 0.10''',
810 # Available variables:
811 #----------------------
812 # payslip: object containing the payslips
813 # employee: hr.employee object
814 # contract: hr.contract object
815 # rules: object containing the rules code (previously computed)
816 # categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category).
817 # worked_days: object containing the computed worked days
818 # inputs: object containing the computed inputs
820 # Note: returned value have to be set in the variable 'result'
822 result = rules.NET > categories.NET * 0.10''',
823 'condition_range': 'contract.wage',
825 'appears_on_payslip': True,
827 'company_id': lambda self, cr, uid, context: \
828 self.pool.get('res.users').browse(cr, uid, uid,
829 context=context).company_id.id,
830 'condition_select': 'none',
831 'amount_select': 'fix',
833 'amount_percentage': 0.0,
837 def _recursive_search_of_rules(self, cr, uid, rule_ids, context=None):
839 @param rule_ids: list of browse record
840 @return: returns a list of tuple (id, sequence) which are all the children of the passed rule_ids
843 for rule in rule_ids:
845 children_rules += self._recursive_search_of_rules(cr, uid, rule.child_ids, context=context)
846 return [(r.id, r.sequence) for r in rule_ids] + children_rules
848 #TODO should add some checks on the type of result (should be float)
849 def compute_rule(self, cr, uid, rule_id, localdict, context=None):
851 :param rule_id: id of rule to compute
852 :param localdict: dictionary containing the environement in which to compute the rule
853 :return: returns a tuple build as the base/amount computed, the quantity and the rate
854 :rtype: (float, float, float)
856 rule = self.browse(cr, uid, rule_id, context=context)
857 if rule.amount_select == 'fix':
859 return rule.amount_fix, eval(rule.quantity, localdict), 100.0
861 raise osv.except_osv(_('Error!'), _('Wrong quantity defined for salary rule %s (%s).')% (rule.name, rule.code))
862 elif rule.amount_select == 'percentage':
864 return eval(rule.amount_percentage_base, localdict), eval(rule.quantity, localdict), rule.amount_percentage
866 raise osv.except_osv(_('Error!'), _('Wrong percentage base or quantity defined for salary rule %s (%s).')% (rule.name, rule.code))
869 eval(rule.amount_python_compute, localdict, mode='exec', nocopy=True)
870 return localdict['result'], 'result_qty' in localdict and localdict['result_qty'] or 1.0, 'result_rate' in localdict and localdict['result_rate'] or 100.0
872 raise osv.except_osv(_('Error!'), _('Wrong python code defined for salary rule %s (%s).')% (rule.name, rule.code))
874 def satisfy_condition(self, cr, uid, rule_id, localdict, context=None):
876 @param rule_id: id of hr.salary.rule to be tested
877 @param contract_id: id of hr.contract to be tested
878 @return: returns True if the given rule match the condition for the given contract. Return False otherwise.
880 rule = self.browse(cr, uid, rule_id, context=context)
882 if rule.condition_select == 'none':
884 elif rule.condition_select == 'range':
886 result = eval(rule.condition_range, localdict)
887 return rule.condition_range_min <= result and result <= rule.condition_range_max or False
889 raise osv.except_osv(_('Error!'), _('Wrong range condition defined for salary rule %s (%s).')% (rule.name, rule.code))
892 eval(rule.condition_python, localdict, mode='exec', nocopy=True)
893 return 'result' in localdict and localdict['result'] or False
895 raise osv.except_osv(_('Error!'), _('Wrong python condition defined for salary rule %s (%s).')% (rule.name, rule.code))
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)
912 class hr_payslip_line(osv.osv):
917 _name = 'hr.payslip.line'
918 _inherit = 'hr.salary.rule'
919 _description = 'Payslip Line'
920 _order = 'contract_id, sequence'
922 def _calculate_total(self, cr, uid, ids, name, args, context):
923 if not ids: return {}
925 for line in self.browse(cr, uid, ids, context=context):
926 res[line.id] = float(line.quantity) * line.amount * line.rate / 100
930 'slip_id':fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade'),
931 'salary_rule_id':fields.many2one('hr.salary.rule', 'Rule', required=True),
932 'employee_id':fields.many2one('hr.employee', 'Employee', required=True),
933 'contract_id':fields.many2one('hr.contract', 'Contract', required=True, select=True),
934 'rate': fields.float('Rate (%)', digits_compute=dp.get_precision('Payroll Rate')),
935 'amount': fields.float('Amount', digits_compute=dp.get_precision('Payroll')),
936 'quantity': fields.float('Quantity', digits_compute=dp.get_precision('Payroll')),
937 'total': fields.function(_calculate_total, method=True, type='float', string='Total', digits_compute=dp.get_precision('Payroll'),store=True ),
947 class hr_employee(osv.osv):
952 _inherit = 'hr.employee'
953 _description = 'Employee'
955 def _calculate_total_wage(self, cr, uid, ids, name, args, context):
956 if not ids: return {}
958 current_date = datetime.now().strftime('%Y-%m-%d')
959 for employee in self.browse(cr, uid, ids, context=context):
960 if not employee.contract_ids:
961 res[employee.id] = {'basic': 0.0}
963 cr.execute( 'SELECT SUM(wage) '\
965 'WHERE employee_id = %s '\
966 'AND date_start <= %s '\
967 'AND (date_end > %s OR date_end is NULL)',
968 (employee.id, current_date, current_date))
969 result = dict(cr.dictfetchone())
970 res[employee.id] = {'basic': result['sum']}
974 'slip_ids':fields.one2many('hr.payslip', 'employee_id', 'Payslips', required=False, readonly=True),
975 '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."),
979 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: