[IMP] hr_payroll: added payslip_sum method to compute 'payslip' on other payslip'
[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_head(osv.osv):
168     """
169     HR Salary Head
170     """
171
172     _name = 'hr.salary.head'
173     _description = 'Salary Head'
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.head', 'Parent', help="Linking a salary head 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_head()
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_head(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_head 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_head': fields.function(_get_lines_salary_head, 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_head(localdict, head, amount):
477             if head.parent_id:
478                 localdict = _sum_salary_head(localdict, head.parent_id, amount)
479             localdict['heads'][head.code] = head.code in localdict['heads'] and localdict['heads'][head.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 = {'heads': {}, '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_head(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     def payslip_sum(self, cr, uid, code, from_date, to_date=datetime.now().strftime('%Y-%m-%d'), employee, context=None):
616         if context is None:
617             context = {}
618         if not employee:
619             employee = 0
620         cr.execute("SELECT sum(pl.total) \
621                     FROM hr_payslip as hp, hr_payslip_line as pl \
622                     WHERE hp.employee_id = %s AND hp.state in ('confirm','done') \
623                     AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id AND pl.code = %s",
624                    (employee, from_date, to_date, code ))
625         res = cr.fetchone()
626         return res and res[0] or 0.0
627     
628 hr_payslip()
629
630 class hr_payslip_input(osv.osv):
631     '''
632     Payslip Input
633     '''
634
635     _name = 'hr.payslip.input'
636     _description = 'Payslip Input'
637     _columns = {
638         'name': fields.char('Description', size=256, required=True),
639         'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True),
640         'sequence': fields.integer('Sequence', required=True,),
641         'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
642         'number_of_days': fields.float('Number of Days'),
643         'number_of_hours': fields.float('Number of Hours'),
644         'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
645     }
646     _order = 'payslip_id,sequence'
647     _defaults = {
648         'sequence': 10,
649     }
650 hr_payslip_input()
651
652 class hr_salary_rule(osv.osv):
653
654     _name = 'hr.salary.rule'
655     _columns = {
656         'name':fields.char('Name', size=256, required=True, readonly=False),
657         'code':fields.char('Code', size=64, required=True),
658         'sequence': fields.integer('Sequence', required=True, help='Use to arrange calculation sequence'),
659         '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']."),
660         'category_id':fields.many2one('hr.salary.head', 'Salary Head', required=True),
661         '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."),
662         'appears_on_payslip': fields.boolean('Appears on Payslip', help="Used for the display of rule on payslip"),
663         'parent_rule_id':fields.many2one('hr.salary.rule', 'Parent Salary Rule', select=True),
664         'company_id':fields.many2one('res.company', 'Company', required=False),
665         'condition_select': fields.selection([('none', 'Always True'),('range', 'Range'), ('python', 'Python Expression')], "Condition Based on", required=True),
666         '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
667         '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
668         'condition_range_min': fields.float('Minimum Range', required=False, help="The minimum amount, applied for this rule."),
669         'condition_range_max': fields.float('Maximum Range', required=False, help="The maximum amount, applied for this rule."),
670         'amount_select':fields.selection([
671             ('percentage','Percentage (%)'),
672             ('fix','Fixed Amount'),
673             ('code','Python Code'),
674         ],'Amount Type', select=True, required=True, help="The computation method for the rule amount."),
675         'amount_fix': fields.float('Fixed Amount', digits_compute=dp.get_precision('Account'),),
676         'amount_percentage': fields.float('Percentage (%)', digits_compute=dp.get_precision('Account'), help='For example, enter 50.0 to apply a percentage of 50%'),
677         'amount_python_compute':fields.text('Python Code'),
678         '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
679         'child_ids':fields.one2many('hr.salary.rule', 'parent_rule_id', 'Child Salary Rule'),
680         'register_id':fields.property(
681             'hr.contribution.register',
682             type='many2one',
683             relation='hr.contribution.register',
684             string="Contribution Register",
685             method=True,
686             view_load=True,
687             help="Contribution register based on company",
688             required=False
689         ),
690         'note':fields.text('Description'),
691      }
692     _defaults = {
693         'amount_python_compute': '''
694 # Available variables:
695 #----------------------
696 # payslip: hr.payslip object
697 # employee: hr.employee object
698 # contract: hr.contract object
699 # rules: rules code (previously computed)
700 # heads: dictionary containing the computed heads (sum of amount of all rules belonging to that head). Keys are the head codes.
701 # worked_days: dictionary containing the computed worked days. Keys are the worked days codes.
702
703 # Note: returned value have to be set in the variable 'result'
704
705 result = contract.wage * 0.10''',
706         'condition_python':
707 '''
708 # Available variables:
709 #----------------------
710 # payslip: hr.payslip object
711 # employee: hr.employee object
712 # contract: hr.contract object
713 # rules: rules code (previously computed)
714 # heads: dictionary containing the computed heads (sum of amount of all rules belonging to that head). Keys are the head codes.
715 # worked_days: dictionary containing the computed worked days. Keys are the worked days codes.
716
717 # Note: returned value have to be set in the variable 'result'
718
719 result = rules['NET'] > heads['NET'] * 0.10''',
720         'condition_range': 'contract.wage',
721         'sequence': 5,
722         'appears_on_payslip': True,
723         'active': True,
724         'company_id': lambda self, cr, uid, context: \
725                 self.pool.get('res.users').browse(cr, uid, uid,
726                     context=context).company_id.id,
727         'condition_select': 'none',
728         'amount_select': 'fix',
729         'amount_fix': 0.0,
730         'amount_percentage': 0.0,
731         'quantity': '1',
732      }
733
734     def _recursive_search_of_rules(self, cr, uid, rule_ids, context=None):
735         """
736         @param rule_ids: list of browse record
737         @return: returns a list of tuple (id, sequence) which are all the children of the passed rule_ids
738         """
739         children_rules = []
740         for rule in rule_ids:
741             if rule.child_ids:
742                 children_rules += self._recursive_search_of_rules(cr, uid, rule.child_ids, context=context)
743         return [(r.id, r.sequence) for r in rule_ids] + children_rules
744
745     #TODO should add some checks on the type of result (should be float)
746     def compute_rule(self, cr, uid, rule_id, localdict, context=None):
747         """
748         @param rule_id: id of rule to compute
749         @param localdict: dictionary containing the environement in which to compute the rule
750         @return: returns the result of computation as float
751         """
752         rule = self.browse(cr, uid, rule_id, context=context)
753         if rule.amount_select == 'fix':
754             try:
755                 return rule.amount_fix * eval(rule.quantity, localdict)
756             except:
757                 raise osv.except_osv(_('Error'), _('Wrong quantity defined for salary rule %s (%s)')% (rule.name, rule.code))
758         elif rule.amount_select == 'percentage':
759             try:
760                 amount = rule.amount_percentage * eval(rule.amount_percentage_base, localdict) / 100
761                 return amount * eval(rule.quantity, localdict)
762             except:
763                 raise osv.except_osv(_('Error'), _('Wrong percentage base or quantity defined for salary rule %s (%s)')% (rule.name, rule.code))
764         else:
765             try:
766                 eval(rule.amount_python_compute, localdict, mode='exec', nocopy=True)
767                 return localdict['result']
768             except:
769                 raise osv.except_osv(_('Error'), _('Wrong python code defined for salary rule %s (%s) ')% (rule.name, rule.code))
770
771     def satisfy_condition(self, cr, uid, rule_id, localdict, context=None):
772         """
773         @param rule_id: id of hr.salary.rule to be tested
774         @param contract_id: id of hr.contract to be tested
775         @return: returns True if the given rule match the condition for the given contract. Return False otherwise.
776         """
777         rule = self.browse(cr, uid, rule_id, context=context)
778
779         if rule.condition_select == 'none':
780             return True
781         elif rule.condition_select == 'range':
782             try:
783                 result = eval(rule.condition_range, localdict)
784                 return rule.condition_range_min <=  result and result <= rule.condition_range_max or False
785             except:
786                 raise osv.except_osv(_('Error'), _('Wrong range condition defined for salary rule %s (%s)')% (rule.name, rule.code))
787         else: #python code
788             try:
789                 eval(rule.condition_python, localdict, mode='exec', nocopy=True)
790                 return 'result' in localdict and localdict['result'] or False
791             except:
792                 raise osv.except_osv(_('Error'), _('Wrong python condition defined for salary rule %s (%s)')% (rule.name, rule.code))
793
794 hr_salary_rule()
795
796 class hr_payslip_line(osv.osv):
797     '''
798     Payslip Line
799     '''
800
801     _name = 'hr.payslip.line'
802     _inherit = 'hr.salary.rule'
803     _description = 'Payslip Line'
804     _order = 'contract_id, sequence'
805
806     _columns = {
807         'slip_id':fields.many2one('hr.payslip', 'Pay Slip', required=True),
808         'salary_rule_id':fields.many2one('hr.salary.rule', 'Rule', required=True),
809         'employee_id':fields.many2one('hr.employee', 'Employee', required=True),
810         'contract_id':fields.many2one('hr.contract', 'Contract', required=True),
811         'total': fields.float('Amount', digits_compute=dp.get_precision('Account')),
812         'company_contrib': fields.float('Company Contribution', readonly=True, digits_compute=dp.get_precision('Account')),
813     }
814
815 hr_payslip_line()
816
817 class hr_payroll_structure(osv.osv):
818
819     _inherit = 'hr.payroll.structure'
820     _columns = {
821         'rule_ids':fields.many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id', 'Salary Rules'),
822     }
823
824 hr_payroll_structure()
825
826 class hr_employee(osv.osv):
827     '''
828     Employee
829     '''
830
831     _inherit = 'hr.employee'
832     _description = 'Employee'
833
834     def _calculate_total_wage(self, cr, uid, ids, name, args, context):
835         if not ids: return {}
836         res = {}
837         current_date = datetime.now().strftime('%Y-%m-%d')
838         for employee in self.browse(cr, uid, ids, context=context):
839             if not employee.contract_ids:
840                 res[employee.id] = {'basic': 0.0}
841                 continue
842             cr.execute( 'SELECT SUM(wage) '\
843                         'FROM hr_contract '\
844                         'WHERE employee_id = %s '\
845                         'AND date_start <= %s '\
846                         'AND (date_end > %s OR date_end is NULL)',
847                          (employee.id, current_date, current_date))
848             result = dict(cr.dictfetchone())
849             res[employee.id] = {'basic': result['sum']}
850         return res
851
852     _columns = {
853         'slip_ids':fields.one2many('hr.payslip', 'employee_id', 'Payslips', required=False, readonly=True),
854         '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."),
855     }
856
857 hr_employee()
858
859 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: