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