[IMP] hr_payroll: renamed salary head into salary category
[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_category(osv.osv):
168     """
169     HR Salary Category
170     """
171
172     _name = 'hr.salary.category'
173     _description = 'Salary 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.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         'sequence': fields.integer('Sequence', required=True, help='Display sequence order'),
181     }
182
183     _defaults = {
184         'company_id': lambda self, cr, uid, context: \
185                 self.pool.get('res.users').browse(cr, uid, uid,
186                     context=context).company_id.id,
187         'sequence': 5
188     }
189
190 hr_salary_category()
191
192 class one2many_mod2(fields.one2many):
193
194     def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
195         if context is None:
196             context = {}
197         if not values:
198             values = {}
199         res = {}
200         for id in ids:
201             res[id] = []
202         ids2 = obj.pool.get(self._obj).search(cr, user, [(self._fields_id,'in',ids), ('appears_on_payslip', '=', True)], limit=self._limit)
203         for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
204             res[r[self._fields_id]].append( r['id'] )
205         return res
206
207 class hr_payslip(osv.osv):
208     '''
209     Pay Slip
210     '''
211
212     _name = 'hr.payslip'
213     _description = 'Pay Slip'
214
215     def _get_lines_salary_category(self, cr, uid, ids, field_names, arg=None, context=None):
216         result = {}
217         if not ids: return result
218         cr.execute('''SELECT pl.slip_id, pl.id FROM hr_payslip_line AS pl \
219                     LEFT JOIN hr_salary_category AS sh on (pl.category_id = sh.id) \
220                     WHERE pl.slip_id in %s \
221                     GROUP BY pl.slip_id, sh.sequence, pl.sequence, pl.id ORDER BY sh.sequence, pl.sequence''',(tuple(ids),))
222         res = cr.fetchall()
223         for r in res:
224             result.setdefault(r[0], [])
225             result[r[0]].append(r[1])
226         return result
227
228     _columns = {
229         'struct_id': fields.many2one('hr.payroll.structure', 'Structure', 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'),
230         'name': fields.char('Description', size=64, required=False, readonly=True, states={'draft': [('readonly', False)]}),
231         'number': fields.char('Reference', size=64, required=False, readonly=True, states={'draft': [('readonly', False)]}),
232         'employee_id': fields.many2one('hr.employee', 'Employee', required=True, readonly=True, states={'draft': [('readonly', False)]}),
233         'date_from': fields.date('Date From', readonly=True, states={'draft': [('readonly', False)]}, required=True),
234         'date_to': fields.date('Date To', readonly=True, states={'draft': [('readonly', False)]}, required=True),
235         'state': fields.selection([
236             ('draft', 'Waiting for Verification'),
237             ('hr_check', 'Waiting for HR Verification'),
238             ('accont_check', 'Waiting for Account Verification'),
239             ('confirm', 'Confirm Sheet'),
240             ('done', 'Paid Salary'),
241             ('cancel', 'Reject'),
242         ], 'State', select=True, readonly=True,
243             help=' * When the payslip is created the state is \'Waiting for verification\'.\
244             \n* It is varified by the user and payslip is sent for HR varification, the state is \'Waiting for HR Verification\'. \
245             \n* If HR varify the payslip, it is sent for account verification, the state is \'Waiting for Account Verification\'. \
246             \n* It is confirmed by the accountant and the state set to \'Confirm Sheet\'.\
247             \n* If the salary is paid then state is set to \'Paid Salary\'.\
248             \n* The \'Reject\' state is used when user cancel payslip.'),
249 #        'line_ids': fields.one2many('hr.payslip.line', 'slip_id', 'Payslip Line', required=False, readonly=True, states={'draft': [('readonly', False)]}),
250         'line_ids': one2many_mod2('hr.payslip.line', 'slip_id', 'Payslip Line',readonly=True, states={'draft':[('readonly',False)]}),
251         'company_id': fields.many2one('res.company', 'Company', required=False, readonly=True, states={'draft': [('readonly', False)]}),
252         'input_line_ids': fields.one2many('hr.payslip.input', 'payslip_id', 'Payslip Inputs', required=False, readonly=True, states={'draft': [('readonly', False)]}),
253         'paid': fields.boolean('Made Payment Order ? ', required=False, readonly=True, states={'draft': [('readonly', False)]}),
254         'note': fields.text('Description'),
255         'contract_id': fields.many2one('hr.contract', 'Contract', required=False, readonly=True, states={'draft': [('readonly', False)]}),
256         'details_by_salary_category': fields.function(_get_lines_salary_category, method=True, type='one2many', relation='hr.payslip.line', string='Details by Salary Head'),
257         'credit_note': fields.boolean('Credit Note', help="Indicates this payslip has a refund of another"),
258     }
259     _defaults = {
260         'date_from': lambda *a: time.strftime('%Y-%m-01'),
261         'date_to': lambda *a: str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10],
262         'state': 'draft',
263         'credit_note': False,
264         'company_id': lambda self, cr, uid, context: \
265                 self.pool.get('res.users').browse(cr, uid, uid,
266                     context=context).company_id.id,
267     }
268
269     def copy(self, cr, uid, id, default=None, context=None):
270         if not default:
271             default = {}
272         company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
273         default.update({
274             'line_ids': [],
275             'move_ids': [],
276             'move_line_ids': [],
277             'company_id': company_id,
278             'period_id': False,
279             'basic_before_leaves': 0.0,
280             'basic_amount': 0.0
281         })
282         return super(hr_payslip, self).copy(cr, uid, id, default, context=context)
283
284     def cancel_sheet(self, cr, uid, ids, context=None):
285         return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
286
287     def account_check_sheet(self, cr, uid, ids, context=None):
288         return self.write(cr, uid, ids, {'state': 'accont_check'}, context=context)
289
290     def hr_check_sheet(self, cr, uid, ids, context=None):
291         return self.write(cr, uid, ids, {'state': 'hr_check'}, context=context)
292
293     def process_sheet(self, cr, uid, ids, context=None):
294         return self.write(cr, uid, ids, {'paid': True, 'state': 'done'}, context=context)
295
296     def refund_sheet(self, cr, uid, ids, context=None):
297         mod_obj = self.pool.get('ir.model.data')
298         wf_service = netsvc.LocalService("workflow")
299         for id in ids:
300             id_copy = self.copy(cr, uid, id, {'credit_note': True}, context=context)
301             self.compute_sheet(cr, uid, [id_copy], context=context)
302             wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'verify_sheet', cr)
303             wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'final_verify_sheet', cr)
304             wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'process_sheet', cr)
305
306         form_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_form')
307         form_res = form_id and form_id[1] or False
308         tree_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_tree')
309         tree_res = tree_id and tree_id[1] or False
310         return {
311             'name':_("Refund Payslip"),
312             'view_mode': 'tree, form',
313             'view_id': False,
314             'view_type': 'form',
315             'res_model': 'hr.payslip',
316             'type': 'ir.actions.act_window',
317             'nodestroy': True,
318             'target': 'current',
319             'domain': "[('id', 'in', %s)]" % [id_copy],
320             'views': [(tree_res, 'tree'), (form_res, 'form')],
321             'context': {}
322         }
323
324     def verify_sheet(self, cr, uid, ids, context=None):
325          #TODO clean me: this function should create the register lines accordingly to the rules computed (run the compute_sheet first)
326 #        holiday_pool = self.pool.get('hr.holidays')
327 #        salary_rule_pool = self.pool.get('hr.salary.rule')
328 #        structure_pool = self.pool.get('hr.payroll.structure')
329 #        register_line_pool = self.pool.get('hr.contibution.register.line')
330 #        contracts = []
331 #        structures = []
332 #        rules = []
333 #        lines = []
334 #        sal_structures =[]
335 #        for slip in self.browse(cr, uid, ids, context=context):
336 #            if slip.contract_id:
337 #                contracts.append(slip.contract_id)
338 #            else:
339 #                contracts = self.get_contract(cr, uid, slip.employee_id, slip.date, context=context)
340 #            for contract in contracts:
341 #                structures.append(contract.struct_id.id)
342 #                leave_ids = self._get_leaves(cr, uid, slip.date, slip.employee_id, contract, context)
343 #                for hday in holiday_pool.browse(cr, uid, leave_ids, context=context):
344 #                    salary_rules = salary_rule_pool.search(cr, uid, [('code', '=', hday.holiday_status_id.code)], context=context)
345 #                    rules +=  salary_rule_pool.browse(cr, uid, salary_rules, context=context)
346 #            for structure in structures:
347 #                sal_structures = self._get_parent_structure(cr, uid, [structure], context=context)
348 #                for struct in sal_structures:
349 #                    lines = structure_pool.browse(cr, uid, struct, context=context).rule_ids
350 #                    for line in lines:
351 #                        if line.child_ids:
352 #                            for r in line.child_ids:
353 #                                lines.append(r)
354 #                        rules.append(line)
355 #            base = {
356 #                'basic': slip.basic_amount,
357 #            }
358 #            if rules:
359 #                for rule in rules:
360 #                    if rule.company_contribution:
361 #                        base[rule.code.lower()] = rule.amount
362 #                        if rule.register_id:
363 #                            for slip in slip.line_ids:
364 #                                if slip.category_id == rule.category_id:
365 #                                    line_tot = slip.total
366 #                            value = eval(rule.computational_expression, base)
367 #                            company_contrib = self._compute(cr, uid, rule.id, value, employee, contract, context)
368 #                            reg_line = {
369 #                                'name': rule.name,
370 #                                'register_id': rule.register_id.id,
371 #                                'code': rule.code,
372 #                                'employee_id': slip.employee_id.id,
373 #                                'emp_deduction': line_tot,
374 #                                'comp_deduction': company_contrib,
375 #                                'total': rule.amount + line_tot
376 #                            }
377 #                            register_line_pool.create(cr, uid, reg_line, context=context)
378         return self.write(cr, uid, ids, {'state': 'confirm'}, context=context)
379
380     #TODO move this function into hr_contract module, on hr.employee object
381     def get_contract(self, cr, uid, employee, date_from, date_to, context=None):
382         """
383         @param employee: browse record of employee
384         @param date_from: date field
385         @param date_to: date field
386         @return: returns the ids of all the contracts for the given employee that need to be considered for the given dates
387         """
388         contract_obj = self.pool.get('hr.contract')
389         clause = []
390         #a contract is valid if it ends between the given dates
391         clause_1 = ['&',('date_end', '<=', date_to),('date_end','>=', date_from)]
392         #OR if it starts between the given dates
393         clause_2 = ['&',('date_start', '<=', date_to),('date_start','>=', date_from)]
394         #OR if it starts before the date_from and finish after the date_end (or never finish)
395         clause_3 = [('date_start','<=', date_from),'|',('date_end', '=', False),('date_end','>=', date)]
396         clause_final =  [('employee_id', '=', employee.id),'|','|'] + clause_1 + clause_2 + clause_3
397         contract_ids = contract_obj.search(cr, uid, [('employee_id', '=', employee.id),], context=context)
398         return contract_ids
399
400     def compute_sheet(self, cr, uid, ids, context=None):
401         slip_line_pool = self.pool.get('hr.payslip.line')
402         for payslip in self.browse(cr, uid, ids, context=context):
403             #delete old payslip lines
404             old_slipline_ids = slip_line_pool.search(cr, uid, [('slip_id', '=', payslip.id)], context=context)
405             old_slipline_ids
406             if old_slipline_ids:
407                 slip_line_pool.unlink(cr, uid, old_slipline_ids, context=context)
408             if payslip.contract_id:
409                 #set the list of contract for which the rules have to be applied
410                 contract_ids = [payslip.contract_id.id]
411             else:
412                 #if we don't give the contract, then the rules to apply should be for all current contracts of the employee
413                 contract_ids = self.get_contract(cr, uid, payslip.employee_id, payslip.date_from, payslip.date_to, context=context)
414             lines = [(0,0,line) for line in self.pool.get('hr.payslip').get_payslip_lines(cr, uid, contract_ids, payslip.id, context=context)]
415             self.write(cr, uid, [payslip.id], {'line_ids': lines}, context=context)
416         return True
417
418     def get_input_lines(self, cr, uid, contract_ids, date_from, date_to, context=None):
419         """
420         @param contract_ids: list of contract id
421         @return: returns a list of dict containing the input that should be applied for the given contract between date_from and date_to
422         """
423         def was_on_leave(employee_id, datetime_day, context=None):
424             res = False
425             day = datetime_day.strftime("%Y-%m-%d")
426             holiday_ids = self.pool.get('hr.holidays').search(cr, uid, [('state','=','validate'),('employee_id','=',employee_id),('type','=','remove'),('date_from','<=',day),('date_to','>=',day)])
427             if holiday_ids:
428                 res = self.pool.get('hr.holidays').browse(cr, uid, holiday_ids, context=context)[0].holiday_status_id.name
429             return res
430
431         res = []
432         for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
433             if not contract.working_hours:
434                 #fill only if the contract as a working schedule linked
435                 continue
436             attendances = {
437                  'name': _("Normal Working Days paid at 100%"),
438                  'sequence': 1,
439                  'code': 'WORK100',
440                  'number_of_days': 0.0,
441                  'number_of_hours': 0.0,
442                  'contract_id': contract.id,
443             }
444             leaves = {}
445             day_from = datetime.strptime(date_from,"%Y-%m-%d")
446             day_to = datetime.strptime(date_to,"%Y-%m-%d")
447             nb_of_days = (day_to - day_from).days + 1
448             for day in range(0, nb_of_days):
449                 working_hours_on_day = self.pool.get('resource.calendar').working_hours_on_day(cr, uid, contract.working_hours, day_from + timedelta(days=day), context)
450                 if working_hours_on_day:
451                     #the employee had to work
452                     leave_type = was_on_leave(contract.employee_id.id, day_from + timedelta(days=day), context=context)
453                     if leave_type:
454                         #if he was on leave, fill the leaves dict
455                         if leave_type in leaves:
456                             leaves[leave_type]['number_of_days'] += 1.0
457                             leaves[leave_type]['number_of_hours'] += working_hours_on_day
458                         else:
459                             leaves[leave_type] = {
460                                 'name': leave_type,
461                                 'sequence': 5,
462                                 'code': leave_type,
463                                 'number_of_days': 1.0,
464                                 'number_of_hours': working_hours_on_day,
465                                 'contract_id': contract.id,
466                             }
467                     else:
468                         #add the input vals to tmp (increment if existing)
469                         attendances['number_of_days'] += 1.0
470                         attendances['number_of_hours'] += working_hours_on_day
471             leaves = [value for key,value in leaves.items()]
472             res += [attendances] + leaves
473         return res
474
475     def get_payslip_lines(self, cr, uid, contract_ids, payslip_id, context):
476         def _sum_salary_category(localdict, category, amount):
477             if category.parent_id:
478                 localdict = _sum_salary_category(localdict, category.parent_id, amount)
479             localdict['categories'][category.code] = category.code in localdict['categories'] and localdict['categories'][category.code] + amount or amount
480             return localdict
481
482         #we keep a dict with the result because a value can be overwritten by another rule with the same code
483         result_dict = {}
484         blacklist = []
485         obj_rule = self.pool.get('hr.salary.rule')
486         payslip = self.pool.get('hr.payslip').browse(cr, uid, payslip_id, context=context)
487         worked_days = {}
488         for input_line in payslip.input_line_ids:
489             worked_days[input_line.code] = input_line
490         localdict = {'categories': {}, 'payslip': payslip, 'worked_days': worked_days}
491         #get the ids of the structures on the contracts and their parent id as well
492         structure_ids = self.pool.get('hr.contract').get_all_structures(cr, uid, contract_ids, context=context)
493         #get the rules of the structure and thier children
494         rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
495         #run the rules by sequence
496         sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
497
498         for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
499             employee = contract.employee_id
500             localdict.update({'employee': employee, 'contract': contract})
501             for rule in obj_rule.browse(cr, uid, sorted_rule_ids, context=context):
502                 key = rule.code + '-' + str(contract.id)
503                 localdict['result'] = None
504                 #check if the rule can be applied
505                 if obj_rule.satisfy_condition(cr, uid, rule.id, localdict, context=context) and rule.id not in blacklist:
506                     #compute the amount of the rule
507                     amount = obj_rule.compute_rule(cr, uid, rule.id, localdict, context=context)
508                     #check if there is already a rule computed with that code
509                     previous_amount = rule.code in localdict and localdict[rule.code] or 0.0
510                     #set/overwrite the amount computed for this rule in the localdict
511                     localdict[rule.code] = amount
512                     #sum the amount for its salary head
513                     localdict = _sum_salary_category(localdict, rule.category_id, amount - previous_amount)
514                     #create/overwrite the rule in the temporary results
515                     result_dict[key] = {
516                         'salary_rule_id': rule.id,
517                         'contract_id': contract.id,
518                         'name': rule.name,
519                         'code': rule.code,
520                         'category_id': rule.category_id.id,
521                         'sequence': rule.sequence,
522                         'appears_on_payslip': rule.appears_on_payslip,
523                         'condition_select': rule.condition_select,
524                         'condition_python': rule.condition_python,
525                         'condition_range': rule.condition_range,
526                         'condition_range_min': rule.condition_range_min,
527                         'condition_range_max': rule.condition_range_max,
528                         'amount_select': rule.amount_select,
529                         'amount_fix': rule.amount_fix,
530                         'amount_python_compute': rule.amount_python_compute,
531                         'amount_percentage': rule.amount_percentage,
532                         'amount_percentage_base': rule.amount_percentage_base,
533                         'register_id': rule.register_id.id,
534                         'total': amount,
535                         'employee_id': contract.employee_id.id,
536                         'quantity': rule.quantity,
537                     }
538                 else:
539                     #blacklist this rule and its children
540                     blacklist += [id for id, seq in self.pool.get('hr.salary.rule')._recursive_search_of_rules(cr, uid, [rule], context=context)]
541
542         result = [value for code, value in result_dict.items()]
543         return result
544
545     def onchange_employee_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
546         empolyee_obj = self.pool.get('hr.employee')
547         contract_obj = self.pool.get('hr.contract')
548         input_obj = self.pool.get('hr.payslip.input')
549
550         if context is None:
551             context = {}
552         #delete old input lines
553         old_input_ids = ids and input_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
554         if old_input_ids:
555             input_obj.unlink(cr, uid, old_input_ids, context=context)
556
557         #defaults
558         res = {'value':{
559                       'line_ids':[],
560                       #'details_by_salary_head':[], TODO put me back
561                       'name':'',
562                       'contract_id': False,
563                       'struct_id': False,
564                       }
565                  }
566         if not employee_id:
567             return res
568         ttyme = datetime.fromtimestamp(time.mktime(time.strptime(date_from, "%Y-%m-%d")))
569         employee_id = empolyee_obj.browse(cr, uid, employee_id, context=context)
570         res['value'].update({
571                            'name': _('Salary Slip of %s for %s') % (employee_id.name, tools.ustr(ttyme.strftime('%B-%Y'))),
572                            'company_id': employee_id.company_id.id
573                            })
574
575         if not context.get('contract', False):
576             #fill with the first contract of the employee
577             contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
578             res['value'].update({
579                     'struct_id': contract_ids and contract_obj.read(cr, uid, contract_ids[0], ['struct_id'], context=context)['struct_id'][0] or False,
580                     'contract_id': contract_ids and contract_ids[0] or False,
581             })
582         else:
583             if contract_id:
584                 #set the list of contract for which the input have to be filled
585                 contract_ids = [contract_id]
586                 #fill the structure with the one on the selected contract
587                 contract_record = contract_obj.browse(cr, uid, contract_id, context=context)
588                 res['value'].update({'struct_id': contract_record.struct_id.id, 'contract_id': contract_id})
589             else:
590                 #if we don't give the contract, then the input to fill should be for all current contracts of the employee
591                 contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
592                 if not contract_ids:
593                     return res
594
595         #computation of the salary input
596         input_line_ids = self.get_input_lines(cr, uid, contract_ids, date_from, date_to, context=context)
597         res['value'].update({
598                     'input_line_ids': input_line_ids,
599             })
600         return res
601
602     def onchange_contract_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
603         if context is None:
604             context = {}
605         res = {'value':{
606                  'line_ids': [],
607                  'name': '',
608                  }
609               }
610         context.update({'contract': True})
611         if not contract_id:
612             res['value'].update({'struct_id': False})
613         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)
614
615 hr_payslip()
616
617 class hr_payslip_input(osv.osv):
618     '''
619     Payslip Input
620     '''
621
622     _name = 'hr.payslip.input'
623     _description = 'Payslip Input'
624     _columns = {
625         'name': fields.char('Description', size=256, required=True),
626         'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True),
627         'sequence': fields.integer('Sequence', required=True,),
628         'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
629         'number_of_days': fields.float('Number of Days'),
630         'number_of_hours': fields.float('Number of Hours'),
631         'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
632     }
633     _order = 'payslip_id,sequence'
634     _defaults = {
635         'sequence': 10,
636     }
637 hr_payslip_input()
638
639 class hr_salary_rule(osv.osv):
640
641     _name = 'hr.salary.rule'
642     _columns = {
643         'name':fields.char('Name', size=256, required=True, readonly=False),
644         'code':fields.char('Code', size=64, required=True),
645         'sequence': fields.integer('Sequence', required=True, help='Use to arrange calculation sequence'),
646         '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']."),
647         'category_id':fields.many2one('hr.salary.category', 'Category', required=True),
648         '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."),
649         'appears_on_payslip': fields.boolean('Appears on Payslip', help="Used for the display of rule on payslip"),
650         'parent_rule_id':fields.many2one('hr.salary.rule', 'Parent Salary Rule', select=True),
651         'company_id':fields.many2one('res.company', 'Company', required=False),
652         'condition_select': fields.selection([('none', 'Always True'),('range', 'Range'), ('python', 'Python Expression')], "Condition Based on", required=True),
653         '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 heads code field in small letter as a variable name i.e. hra, ma, lta, etc...., also you can use, static varible basic'),#old name = conputional expression
654         '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
655         'condition_range_min': fields.float('Minimum Range', required=False, help="The minimum amount, applied for this rule."),
656         'condition_range_max': fields.float('Maximum Range', required=False, help="The maximum amount, applied for this rule."),
657         'amount_select':fields.selection([
658             ('percentage','Percentage (%)'),
659             ('fix','Fixed Amount'),
660             ('code','Python Code'),
661         ],'Amount Type', select=True, required=True, help="The computation method for the rule amount."),
662         'amount_fix': fields.float('Fixed Amount', digits_compute=dp.get_precision('Account'),),
663         'amount_percentage': fields.float('Percentage (%)', digits_compute=dp.get_precision('Account'), help='For example, enter 50.0 to apply a percentage of 50%'),
664         'amount_python_compute':fields.text('Python Code'),
665         'amount_percentage_base':fields.char('Percentage based on',size=1024, required=False, readonly=False, help='result will be affected to a variable'), #old name = expressiont
666         'child_ids':fields.one2many('hr.salary.rule', 'parent_rule_id', 'Child Salary Rule'),
667         'register_id':fields.property(
668             'hr.contribution.register',
669             type='many2one',
670             relation='hr.contribution.register',
671             string="Contribution Register",
672             method=True,
673             view_load=True,
674             help="Contribution register based on company",
675             required=False
676         ),
677         'note':fields.text('Description'),
678      }
679     _defaults = {
680         'amount_python_compute': '''
681 # Available variables:
682 #----------------------
683 # payslip: hr.payslip object
684 # employee: hr.employee object
685 # contract: hr.contract object
686 # rules: rules code (previously computed)
687 # categories: dictionary containing the computed categories (sum of amount of all rules belonging to that category). Keys are the category codes.
688 # worked_days: dictionary containing the computed worked days. Keys are the worked days codes.
689
690 # Note: returned value have to be set in the variable 'result'
691
692 result = contract.wage * 0.10''',
693         'condition_python':
694 '''
695 # Available variables:
696 #----------------------
697 # payslip: hr.payslip object
698 # employee: hr.employee object
699 # contract: hr.contract object
700 # rules: rules code (previously computed)
701 # categories: dictionary containing the computed categories (sum of amount of all rules belonging to that category). Keys are the category codes.
702 # worked_days: dictionary containing the computed worked days. Keys are the worked days codes.
703
704 # Note: returned value have to be set in the variable 'result'
705
706 result = rules['NET'] > categories['NET'] * 0.10''',
707         'condition_range': 'contract.wage',
708         'sequence': 5,
709         'appears_on_payslip': True,
710         'active': True,
711         'company_id': lambda self, cr, uid, context: \
712                 self.pool.get('res.users').browse(cr, uid, uid,
713                     context=context).company_id.id,
714         'condition_select': 'none',
715         'amount_select': 'fix',
716         'amount_fix': 0.0,
717         'amount_percentage': 0.0,
718         'quantity': '1',
719      }
720
721     def _recursive_search_of_rules(self, cr, uid, rule_ids, context=None):
722         """
723         @param rule_ids: list of browse record
724         @return: returns a list of tuple (id, sequence) which are all the children of the passed rule_ids
725         """
726         children_rules = []
727         for rule in rule_ids:
728             if rule.child_ids:
729                 children_rules += self._recursive_search_of_rules(cr, uid, rule.child_ids, context=context)
730         return [(r.id, r.sequence) for r in rule_ids] + children_rules
731
732     #TODO should add some checks on the type of result (should be float)
733     def compute_rule(self, cr, uid, rule_id, localdict, context=None):
734         """
735         @param rule_id: id of rule to compute
736         @param localdict: dictionary containing the environement in which to compute the rule
737         @return: returns the result of computation as float
738         """
739         rule = self.browse(cr, uid, rule_id, context=context)
740         if rule.amount_select == 'fix':
741             try:
742                 return rule.amount_fix * eval(rule.quantity, localdict)
743             except:
744                 raise osv.except_osv(_('Error'), _('Wrong quantity defined for salary rule %s (%s)')% (rule.name, rule.code))
745         elif rule.amount_select == 'percentage':
746             try:
747                 amount = rule.amount_percentage * eval(rule.amount_percentage_base, localdict) / 100
748                 return amount * eval(rule.quantity, localdict)
749             except:
750                 raise osv.except_osv(_('Error'), _('Wrong percentage base or quantity defined for salary rule %s (%s)')% (rule.name, rule.code))
751         else:
752             try:
753                 eval(rule.amount_python_compute, localdict, mode='exec', nocopy=True)
754                 return localdict['result']
755             except:
756                 raise osv.except_osv(_('Error'), _('Wrong python code defined for salary rule %s (%s) ')% (rule.name, rule.code))
757
758     def satisfy_condition(self, cr, uid, rule_id, localdict, context=None):
759         """
760         @param rule_id: id of hr.salary.rule to be tested
761         @param contract_id: id of hr.contract to be tested
762         @return: returns True if the given rule match the condition for the given contract. Return False otherwise.
763         """
764         rule = self.browse(cr, uid, rule_id, context=context)
765
766         if rule.condition_select == 'none':
767             return True
768         elif rule.condition_select == 'range':
769             try:
770                 result = eval(rule.condition_range, localdict)
771                 return rule.condition_range_min <=  result and result <= rule.condition_range_max or False
772             except:
773                 raise osv.except_osv(_('Error'), _('Wrong range condition defined for salary rule %s (%s)')% (rule.name, rule.code))
774         else: #python code
775             try:
776                 eval(rule.condition_python, localdict, mode='exec', nocopy=True)
777                 return 'result' in localdict and localdict['result'] or False
778             except:
779                 raise osv.except_osv(_('Error'), _('Wrong python condition defined for salary rule %s (%s)')% (rule.name, rule.code))
780
781 hr_salary_rule()
782
783 class hr_payslip_line(osv.osv):
784     '''
785     Payslip Line
786     '''
787
788     _name = 'hr.payslip.line'
789     _inherit = 'hr.salary.rule'
790     _description = 'Payslip Line'
791     _order = 'contract_id, sequence'
792
793     _columns = {
794         'slip_id':fields.many2one('hr.payslip', 'Pay Slip', required=True),
795         'salary_rule_id':fields.many2one('hr.salary.rule', 'Rule', required=True),
796         'employee_id':fields.many2one('hr.employee', 'Employee', required=True),
797         'contract_id':fields.many2one('hr.contract', 'Contract', required=True),
798         'total': fields.float('Amount', digits_compute=dp.get_precision('Account')),
799         'company_contrib': fields.float('Company Contribution', readonly=True, digits_compute=dp.get_precision('Account')),
800     }
801
802 hr_payslip_line()
803
804 class hr_payroll_structure(osv.osv):
805
806     _inherit = 'hr.payroll.structure'
807     _columns = {
808         'rule_ids':fields.many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id', 'Salary Rules'),
809     }
810
811 hr_payroll_structure()
812
813 class hr_employee(osv.osv):
814     '''
815     Employee
816     '''
817
818     _inherit = 'hr.employee'
819     _description = 'Employee'
820
821     def _calculate_total_wage(self, cr, uid, ids, name, args, context):
822         if not ids: return {}
823         res = {}
824         current_date = datetime.now().strftime('%Y-%m-%d')
825         for employee in self.browse(cr, uid, ids, context=context):
826             if not employee.contract_ids:
827                 res[employee.id] = {'basic': 0.0}
828                 continue
829             cr.execute( 'SELECT SUM(wage) '\
830                         'FROM hr_contract '\
831                         'WHERE employee_id = %s '\
832                         'AND date_start <= %s '\
833                         'AND (date_end > %s OR date_end is NULL)',
834                          (employee.id, current_date, current_date))
835             result = dict(cr.dictfetchone())
836             res[employee.id] = {'basic': result['sum']}
837         return res
838
839     _columns = {
840         'slip_ids':fields.one2many('hr.payslip', 'employee_id', 'Payslips', required=False, readonly=True),
841         '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."),
842     }
843
844 hr_employee()
845
846 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: