[MERGE] hr_payroll: changed field 'quantity' into 'amount' in input lines
[odoo/odoo.git] / addons / hr_payroll / hr_payroll.py
1 #-*- coding:utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
6 #    d$
7 #
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.
12 #
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.
17 #
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/>.
20 #
21 ##############################################################################
22
23 import time
24 from datetime import date
25 from datetime import datetime
26 from datetime import timedelta
27 from dateutil import relativedelta
28
29 import netsvc
30 from osv import fields, osv
31 import tools
32 from tools.translate import _
33 import decimal_precision as dp
34
35 from tools.safe_eval import safe_eval as eval
36
37 class hr_payroll_structure(osv.osv):
38     """
39     Salary structure used to defined
40     - Basic
41     - Allowances
42     - Deductions
43     """
44
45     _name = 'hr.payroll.structure'
46     _description = 'Salary Structure'
47     _columns = {
48         'name':fields.char('Name', size=256, required=True),
49         'code':fields.char('Reference', size=64, required=True),
50         'company_id':fields.many2one('res.company', 'Company', required=True),
51         'note': fields.text('Description'),
52         'parent_id':fields.many2one('hr.payroll.structure', 'Parent'),
53     }
54
55     def _get_parent(self, cr, uid, context=None):
56         obj_model = self.pool.get('ir.model.data')
57         res = False
58         data_id = obj_model.search(cr, uid, [('model', '=', 'hr.payroll.structure'), ('name', '=', 'structure_base')])
59         if data_id:
60             res = obj_model.browse(cr, uid, data_id[0], context=context).res_id
61         return res
62
63     _defaults = {
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,
68     }
69
70     def copy(self, cr, uid, id, default=None, context=None):
71         """
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
78
79         @return: returns a id of newly created record
80         """
81         if not default:
82             default = {}
83         default.update({
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
86         })
87         return super(hr_payroll_structure, self).copy(cr, uid, id, default, context=context)
88
89     def get_all_rules(self, cr, uid, structure_ids, context=None):
90         """
91         @param structure_ids: list of structure
92         @return: returns a list of tuple (id, sequence) of rules that are maybe to apply
93         """
94
95         all_rules = []
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)
98         return all_rules
99
100     def _get_parent_structure(self, cr, uid, struct_ids, context=None):
101         if not struct_ids:
102             return []
103         parent = []
104         for struct in self.browse(cr, uid, struct_ids, context=context):
105             if struct.parent_id:
106                 parent.append(struct.parent_id.id)
107         if parent:
108             parent = self._get_parent_structure(cr, uid, parent, context)
109         return parent + struct_ids
110
111 hr_payroll_structure()
112
113 class hr_contract(osv.osv):
114     """
115     Employee contract based on the visa, work permits
116     allows to configure different Salary structure
117     """
118
119     _inherit = 'hr.contract'
120     _description = 'Employee Contract'
121     _columns = {
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),
132     }
133
134     def get_all_structures(self, cr, uid, contract_ids, context=None):
135         """
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
138         """
139         all_structures = []
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)))
142
143 hr_contract()
144
145 class contrib_register(osv.osv):
146     '''
147     Contribution Register
148     '''
149
150     _name = 'hr.contribution.register'
151     _description = 'Contribution Register'
152
153     _columns = {
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'),
158     }
159     _defaults = {
160         'company_id': lambda self, cr, uid, context: \
161                 self.pool.get('res.users').browse(cr, uid, uid,
162                     context=context).company_id.id,
163     }
164
165 contrib_register()
166
167 class hr_salary_rule_category(osv.osv):
168     """
169     HR Salary Rule Category
170     """
171
172     _name = 'hr.salary.rule.category'
173     _description = 'Salary Rule Category'
174     _columns = {
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),
180     }
181
182     _defaults = {
183         'company_id': lambda self, cr, uid, context: \
184                 self.pool.get('res.users').browse(cr, uid, uid,
185                     context=context).company_id.id,
186     }
187
188 hr_salary_rule_category()
189
190 class one2many_mod2(fields.one2many):
191
192     def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
193         if context is None:
194             context = {}
195         if not values:
196             values = {}
197         res = {}
198         for id in ids:
199             res[id] = []
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'] )
203         return res
204
205 class hr_payslip_run(osv.osv):
206
207     _name = 'hr.payslip.run'
208     _columns = {
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([
212             ('draft', 'Draft'),
213             ('close', 'Close'),
214         ], 'State', select=True, readonly=True)
215     }
216     _defaults = {
217         'state': 'draft',
218     }
219
220     def draft_payslip_run(self, cr, uid, ids, context=None):
221         return self.write(cr, uid, ids, {'state': 'draft'}, context=context)
222
223     def close_payslip_run(self, cr, uid, ids, context=None):
224         return self.write(cr, uid, ids, {'state': 'close'}, context=context)
225
226 hr_payslip_run()
227
228 class hr_payslip(osv.osv):
229     '''
230     Pay Slip
231     '''
232
233     _name = 'hr.payslip'
234     _description = 'Pay Slip'
235
236     def _get_lines_salary_rule_category(self, cr, uid, ids, field_names, arg=None, context=None):
237         result = {}
238         if not ids: return result
239         for id in ids:
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),))
245         res = cr.fetchall()
246         for r in res:
247             result[r[0]].append(r[1])
248         return result
249
250     _columns = {
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([
258             ('draft', 'Draft'),
259             ('verify', 'Waiting'),
260             ('done', 'Done'),
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)]}),
278     }
279     _defaults = {
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],
282         'state': 'draft',
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,
287     }
288
289     def copy(self, cr, uid, id, default=None, context=None):
290         if not default:
291             default = {}
292         company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
293         default.update({
294             'line_ids': [],
295             'move_ids': [],
296             'move_line_ids': [],
297             'company_id': company_id,
298             'period_id': False,
299             'basic_before_leaves': 0.0,
300             'basic_amount': 0.0
301         })
302         return super(hr_payslip, self).copy(cr, uid, id, default, context=context)
303
304     def cancel_sheet(self, cr, uid, ids, context=None):
305         return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
306
307     def account_check_sheet(self, cr, uid, ids, context=None):
308         return self.write(cr, uid, ids, {'state': 'accont_check'}, context=context)
309     
310     def hr_check_sheet(self, cr, uid, ids, context=None):
311         return self.write(cr, uid, ids, {'state': 'hr_check'}, context=context)
312     
313     def process_sheet(self, cr, uid, ids, context=None):
314         return self.write(cr, uid, ids, {'paid': True, 'state': 'done'}, context=context)
315
316     def hr_verify_sheet(self, cr, uid, ids, context=None):
317         return self.write(cr, uid, ids, {'state': 'verify'}, context=context)
318     
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")
322         for id in ids:
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)
327
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
332         return {
333             'name':_("Refund Payslip"),
334             'view_mode': 'tree, form',
335             'view_id': False,
336             'view_type': 'form',
337             'res_model': 'hr.payslip',
338             'type': 'ir.actions.act_window',
339             'nodestroy': True,
340             'target': 'current',
341             'domain': "[('id', 'in', %s)]" % [id_copy],
342             'views': [(tree_res, 'tree'), (form_res, 'form')],
343             'context': {}
344         }
345
346     def verify_sheet(self, cr, uid, ids, context=None):
347         return self.write(cr, uid, ids, {'state': 'confirm'}, context=context)
348     
349     def check_done(self, cr, uid, ids, context=None):
350         return True
351
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):
354         """
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
359         """
360         contract_obj = self.pool.get('hr.contract')
361         clause = []
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)
370         return contract_ids
371
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)
379 #            old_slipline_ids
380             if old_slipline_ids:
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]
385             else:
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)
390         return True
391
392     def get_worked_day_lines(self, cr, uid, contract_ids, date_from, date_to, context=None):
393         """
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
396         """
397         def was_on_leave(employee_id, datetime_day, context=None):
398             res = False
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)])
401             if holiday_ids:
402                 res = self.pool.get('hr.holidays').browse(cr, uid, holiday_ids, context=context)[0].holiday_status_id.name
403             return res
404
405         res = []
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
409                 continue
410             attendances = {
411                  'name': _("Normal Working Days paid at 100%"),
412                  'sequence': 1,
413                  'code': 'WORK100',
414                  'number_of_days': 0.0,
415                  'number_of_hours': 0.0,
416                  'contract_id': contract.id,
417             }
418             leaves = {}
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)
427                     if leave_type:
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
432                         else:
433                             leaves[leave_type] = {
434                                 'name': leave_type,
435                                 'sequence': 5,
436                                 'code': leave_type,
437                                 'number_of_days': 1.0,
438                                 'number_of_hours': working_hours_on_day,
439                                 'contract_id': contract.id,
440                             }
441                     else:
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
447         return res
448
449     def get_inputs(self, cr, uid, contract_ids, date_from, date_to, context=None):
450         res = []
451         contract_obj = self.pool.get('hr.contract')
452         rule_obj = self.pool.get('hr.salary.rule')
453
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])]
457
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):
460                 if rule.input_ids:
461                     for input in rule.input_ids:
462                         inputs = {
463                              'name': input.name,
464                              'code': input.code,
465                              'contract_id': contract.id,
466                         }
467                         res += [inputs]
468         return res
469
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
475             return localdict
476
477         class BrowsableObject(object):
478             def __init__(self, pool, cr, uid, employee_id, dict):
479                 self.pool = pool
480                 self.cr = cr
481                 self.uid = uid
482                 self.employee_id = employee_id
483                 self.dict = dict
484
485             def __getattr__(self, attr):
486                 return self.dict.__getitem__(attr)
487
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):
491                 if to_date is None:
492                     to_date = datetime.now().strftime('%Y-%m-%d')
493                 result = 0.0
494                 self.cr.execute("SELECT sum(amount) 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]
500                 return res or 0.0
501
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):
505                 if to_date is None:
506                     to_date = datetime.now().strftime('%Y-%m-%d')
507                 result = 0.0
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()
514
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
518
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
522
523         class Payslips(BrowsableObject):
524             """a class that will be used into the python code, mainly for usability purposes"""
525
526             def sum(self, code, from_date, to_date=None):
527                 if to_date is 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
536
537         #we keep a dict with the result because a value can be overwritten by another rule with the same code
538         result_dict = {}
539         blacklist = []
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)
544         worked_days = {}
545         for worked_days_line in payslip.worked_days_line_ids:
546             worked_days[worked_days_line.code] = worked_days_line
547         inputs = {}
548         for input_line in payslip.input_line_ids:
549             inputs[input_line.code] = input_line
550
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)
554
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])]
562
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
580                     result_dict[key] = {
581                         'salary_rule_id': rule.id,
582                         'contract_id': contract.id,
583                         'name': rule.name,
584                         'code': rule.code,
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,
599                         'total': amount,
600                         'employee_id': contract.employee_id.id,
601                         'quantity': rule.quantity,
602                     }
603                 else:
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)]
606
607         result = [value for code, value in result_dict.items()]
608         return result
609
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')
615
616         if context is None:
617             context = {}
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)
622
623         #delete old input lines
624         old_input_ids = ids and input_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
625         if old_input_ids:
626             input_obj.unlink(cr, uid, old_input_ids, context=context)
627
628
629         #defaults
630         res = {'value':{
631                       'line_ids':[],
632                       'input_line_ids': [],
633                       'worked_days_line_ids': [],
634                       #'details_by_salary_head':[], TODO put me back
635                       'name':'',
636                       'contract_id': False,
637                       'struct_id': False,
638                       }
639             }
640         if not employee_id:
641             return res
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
647         })
648
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,
655             })
656         else:
657             if contract_id:
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
665                 })
666             else:
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)
669                 if not contract_ids:
670                     return res
671
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,
678         })
679         return res
680
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
683         if context is None:
684             context = {}
685         res = {'value':{
686                  'line_ids': [],
687                  'name': '',
688                  }
689               }
690         context.update({'contract': True})
691         if not contract_id:
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)
694
695 hr_payslip()
696
697 class hr_payslip_worked_days(osv.osv):
698     '''
699     Payslip Worked Days
700     '''
701
702     _name = 'hr.payslip.worked_days'
703     _description = 'Payslip Worked Days'
704     _columns = {
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"),
712     }
713     _order = 'payslip_id, sequence'
714     _defaults = {
715         'sequence': 10,
716     }
717 hr_payslip_worked_days()
718
719 class hr_payslip_input(osv.osv):
720     '''
721     Payslip Input
722     '''
723
724     _name = 'hr.payslip.input'
725     _description = 'Payslip Input'
726     _columns = {
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         '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."),
732         'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
733     }
734     _order = 'payslip_id, sequence'
735     _defaults = {
736         'sequence': 10,
737         'amount': 0.0,
738     }
739
740 hr_payslip_input()
741
742 class hr_salary_rule(osv.osv):
743
744     _name = 'hr.salary.rule'
745     _columns = {
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',
772             type='many2one',
773             relation='hr.contribution.register',
774             string="Contribution Register",
775             method=True,
776             view_load=True,
777             help="Contribution register based on company",
778             required=False
779         ),
780         'input_ids': fields.one2many('hr.rule.input', 'input_id', 'Inputs'),
781         'note':fields.text('Description'),
782      }
783     _defaults = {
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.
794
795 # Note: returned value have to be set in the variable 'result'
796
797 result = contract.wage * 0.10''',
798         'condition_python':
799 '''
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
809
810 # Note: returned value have to be set in the variable 'result'
811
812 result = rules['NET'] > categories['NET'] * 0.10''',
813         'condition_range': 'contract.wage',
814         'sequence': 5,
815         'appears_on_payslip': True,
816         'active': 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',
822         'amount_fix': 0.0,
823         'amount_percentage': 0.0,
824         'quantity': '1',
825      }
826
827     def _recursive_search_of_rules(self, cr, uid, rule_ids, context=None):
828         """
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
831         """
832         children_rules = []
833         for rule in rule_ids:
834             if rule.child_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
837
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):
840         """
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
844         """
845         rule = self.browse(cr, uid, rule_id, context=context)
846         if rule.amount_select == 'fix':
847             try:
848                 return rule.amount_fix * eval(rule.quantity, localdict)
849             except:
850                 raise osv.except_osv(_('Error'), _('Wrong quantity defined for salary rule %s (%s)')% (rule.name, rule.code))
851         elif rule.amount_select == 'percentage':
852             try:
853                 amount = rule.amount_percentage * eval(rule.amount_percentage_base, localdict) / 100
854                 return amount * eval(rule.quantity, localdict)
855             except:
856                 raise osv.except_osv(_('Error'), _('Wrong percentage base or quantity defined for salary rule %s (%s)')% (rule.name, rule.code))
857         else:
858             try:
859                 eval(rule.amount_python_compute, localdict, mode='exec', nocopy=True)
860                 return localdict['result']
861             except:
862                 raise osv.except_osv(_('Error'), _('Wrong python code defined for salary rule %s (%s) ')% (rule.name, rule.code))
863
864     def satisfy_condition(self, cr, uid, rule_id, localdict, context=None):
865         """
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.
869         """
870         rule = self.browse(cr, uid, rule_id, context=context)
871
872         if rule.condition_select == 'none':
873             return True
874         elif rule.condition_select == 'range':
875             try:
876                 result = eval(rule.condition_range, localdict)
877                 return rule.condition_range_min <=  result and result <= rule.condition_range_max or False
878             except:
879                 raise osv.except_osv(_('Error'), _('Wrong range condition defined for salary rule %s (%s)')% (rule.name, rule.code))
880         else: #python code
881             try:
882                 eval(rule.condition_python, localdict, mode='exec', nocopy=True)
883                 return 'result' in localdict and localdict['result'] or False
884             except:
885                 raise osv.except_osv(_('Error'), _('Wrong python condition defined for salary rule %s (%s)')% (rule.name, rule.code))
886
887 hr_salary_rule()
888
889 class hr_rule_input(osv.osv):
890     '''
891     Salary Rule Input
892     '''
893
894     _name = 'hr.rule.input'
895     _description = 'Salary Rule Input'
896     _columns = {
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)
900     }
901
902 hr_rule_input()
903
904 class hr_payslip_line(osv.osv):
905     '''
906     Payslip Line
907     '''
908
909     _name = 'hr.payslip.line'
910     _inherit = 'hr.salary.rule'
911     _description = 'Payslip Line'
912     _order = 'contract_id, sequence'
913
914     _columns = {
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')),
921     }
922
923 hr_payslip_line()
924
925 class hr_payroll_structure(osv.osv):
926
927     _inherit = 'hr.payroll.structure'
928     _columns = {
929         'rule_ids':fields.many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id', 'Salary Rules'),
930     }
931
932 hr_payroll_structure()
933
934 class hr_employee(osv.osv):
935     '''
936     Employee
937     '''
938
939     _inherit = 'hr.employee'
940     _description = 'Employee'
941
942     def _calculate_total_wage(self, cr, uid, ids, name, args, context):
943         if not ids: return {}
944         res = {}
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}
949                 continue
950             cr.execute( 'SELECT SUM(wage) '\
951                         'FROM hr_contract '\
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']}
958         return res
959
960     _columns = {
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."),
963     }
964
965 hr_employee()
966
967 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: