[FIX] l10n_hn: Set the right 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         'children_ids':fields.one2many('hr.payroll.structure', 'parent_id', 'Children'),
54     }
55
56     def _get_parent(self, cr, uid, context=None):
57         obj_model = self.pool.get('ir.model.data')
58         res = False
59         data_id = obj_model.search(cr, uid, [('model', '=', 'hr.payroll.structure'), ('name', '=', 'structure_base')])
60         if data_id:
61             res = obj_model.browse(cr, uid, data_id[0], context=context).res_id
62         return res
63
64     _defaults = {
65         'company_id': lambda self, cr, uid, context: \
66                 self.pool.get('res.users').browse(cr, uid, uid,
67                     context=context).company_id.id,
68         'parent_id': _get_parent,
69     }
70
71     def copy(self, cr, uid, id, default=None, context=None):
72         """
73         Create a new record in hr_payroll_structure model from existing one
74         @param cr: cursor to database
75         @param user: id of current user
76         @param id: list of record ids on which copy method executes
77         @param default: dict type contains the values to be override during copy of object
78         @param context: context arguments, like lang, time zone
79
80         @return: returns a id of newly created record
81         """
82         if not default:
83             default = {}
84         default.update({
85             'code': self.browse(cr, uid, id, context=context).code + "(copy)",
86             'company_id': self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
87         })
88         return super(hr_payroll_structure, self).copy(cr, uid, id, default, context=context)
89
90     def get_all_rules(self, cr, uid, structure_ids, context=None):
91         """
92         @param structure_ids: list of structure
93         @return: returns a list of tuple (id, sequence) of rules that are maybe to apply
94         """
95
96         all_rules = []
97         for struct in self.browse(cr, uid, structure_ids, context=context):
98             all_rules += self.pool.get('hr.salary.rule')._recursive_search_of_rules(cr, uid, struct.rule_ids, context=context)
99         return all_rules
100
101     def _get_parent_structure(self, cr, uid, struct_ids, context=None):
102         if not struct_ids:
103             return []
104         parent = []
105         for struct in self.browse(cr, uid, struct_ids, context=context):
106             if struct.parent_id:
107                 parent.append(struct.parent_id.id)
108         if parent:
109             parent = self._get_parent_structure(cr, uid, parent, context)
110         return parent + struct_ids
111
112 hr_payroll_structure()
113
114 class hr_contract(osv.osv):
115     """
116     Employee contract based on the visa, work permits
117     allows to configure different Salary structure
118     """
119
120     _inherit = 'hr.contract'
121     _description = 'Employee Contract'
122     _columns = {
123         'struct_id': fields.many2one('hr.payroll.structure', 'Salary Structure'),
124         'schedule_pay': fields.selection([
125             ('monthly', 'Monthly'),
126             ('quarterly', 'Quarterly'),
127             ('semi-annually', 'Semi-annually'),
128             ('annually', 'Annually'),
129             ('weekly', 'Weekly'),
130             ('bi-weekly', 'Bi-weekly'),
131             ('bi-monthly', 'Bi-monthly'),
132             ], 'Scheduled Pay', select=True),
133     }
134
135     _defaults = {
136         'schedule_pay': 'monthly',
137     }
138
139     def get_all_structures(self, cr, uid, contract_ids, context=None):
140         """
141         @param contract_ids: list of contracts
142         @return: the structures linked to the given contracts, ordered by hierachy (parent=False first, then first level children and so on) and without duplicata
143         """
144         all_structures = []
145         structure_ids = [contract.struct_id.id for contract in self.browse(cr, uid, contract_ids, context=context)]
146         return list(set(self.pool.get('hr.payroll.structure')._get_parent_structure(cr, uid, structure_ids, context=context)))
147
148 hr_contract()
149
150 class contrib_register(osv.osv):
151     '''
152     Contribution Register
153     '''
154
155     _name = 'hr.contribution.register'
156     _description = 'Contribution Register'
157
158     _columns = {
159         'company_id':fields.many2one('res.company', 'Company', required=False),
160         'name':fields.char('Name', size=256, required=True, readonly=False),
161         'register_line_ids':fields.one2many('hr.payslip.line', 'register_id', 'Register Line', readonly=True),
162         'note': fields.text('Description'),
163     }
164     _defaults = {
165         'company_id': lambda self, cr, uid, context: \
166                 self.pool.get('res.users').browse(cr, uid, uid,
167                     context=context).company_id.id,
168     }
169
170 contrib_register()
171
172 class hr_salary_rule_category(osv.osv):
173     """
174     HR Salary Rule Category
175     """
176
177     _name = 'hr.salary.rule.category'
178     _description = 'Salary Rule Category'
179     _columns = {
180         'name':fields.char('Name', size=64, required=True, readonly=False),
181         'code':fields.char('Code', size=64, required=True, readonly=False),
182         'parent_id':fields.many2one('hr.salary.rule.category', 'Parent', help="Linking a salary category to its parent is used only for the reporting purpose."),
183         'children_ids': fields.one2many('hr.salary.rule.category', 'parent_id', 'Children'),
184         'note': fields.text('Description'),
185         'company_id':fields.many2one('res.company', 'Company', required=False),
186     }
187
188     _defaults = {
189         'company_id': lambda self, cr, uid, context: \
190                 self.pool.get('res.users').browse(cr, uid, uid,
191                     context=context).company_id.id,
192     }
193
194 hr_salary_rule_category()
195
196 class one2many_mod2(fields.one2many):
197
198     def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
199         if context is None:
200             context = {}
201         if not values:
202             values = {}
203         res = {}
204         for id in ids:
205             res[id] = []
206         ids2 = obj.pool.get(self._obj).search(cr, user, [(self._fields_id,'in',ids), ('appears_on_payslip', '=', True)], limit=self._limit)
207         for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
208             res[r[self._fields_id]].append( r['id'] )
209         return res
210
211 class hr_payslip_run(osv.osv):
212
213     _name = 'hr.payslip.run'
214     _description = 'Payslip Run'
215     _columns = {
216         'name': fields.char('Name', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
217         'slip_ids': fields.one2many('hr.payslip', 'payslip_run_id', 'Payslips', required=False, readonly=True, states={'draft': [('readonly', False)]}),
218         'state': fields.selection([
219             ('draft', 'Draft'),
220             ('close', 'Close'),
221         ], 'State', select=True, readonly=True),
222         'date_start': fields.date('Date From', required=True, readonly=True, states={'draft': [('readonly', False)]}),
223         'date_end': fields.date('Date To', required=True, readonly=True, states={'draft': [('readonly', False)]}),
224         'credit_note': fields.boolean('Credit Note', readonly=True, states={'draft': [('readonly', False)]}, help="If its checked, indicates that all payslips generated from here are refund payslips."),
225     }
226     _defaults = {
227         'state': 'draft',
228         'date_start': lambda *a: time.strftime('%Y-%m-01'),
229         'date_end': lambda *a: str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10],
230     }
231
232     def draft_payslip_run(self, cr, uid, ids, context=None):
233         return self.write(cr, uid, ids, {'state': 'draft'}, context=context)
234
235     def close_payslip_run(self, cr, uid, ids, context=None):
236         return self.write(cr, uid, ids, {'state': 'close'}, context=context)
237
238 hr_payslip_run()
239
240 class hr_payslip(osv.osv):
241     '''
242     Pay Slip
243     '''
244
245     _name = 'hr.payslip'
246     _description = 'Pay Slip'
247
248     def _get_lines_salary_rule_category(self, cr, uid, ids, field_names, arg=None, context=None):
249         result = {}
250         if not ids: return result
251         for id in ids:
252             result.setdefault(id, [])
253         cr.execute('''SELECT pl.slip_id, pl.id FROM hr_payslip_line AS pl \
254                     LEFT JOIN hr_salary_rule_category AS sh on (pl.category_id = sh.id) \
255                     WHERE pl.slip_id in %s \
256                     GROUP BY pl.slip_id, pl.sequence, pl.id ORDER BY pl.sequence''',(tuple(ids),))
257         res = cr.fetchall()
258         for r in res:
259             result[r[0]].append(r[1])
260         return result
261
262     _columns = {
263         'struct_id': fields.many2one('hr.payroll.structure', 'Structure', readonly=True, states={'draft': [('readonly', False)]}, help='Defines the rules that have to be applied to this payslip, accordingly to the contract chosen. If you let empty the field contract, this field isn\'t mandatory anymore and thus the rules applied will be all the rules set on the structure of all contracts of the employee valid for the chosen period'),
264         'name': fields.char('Description', size=64, required=False, readonly=True, states={'draft': [('readonly', False)]}),
265         'number': fields.char('Reference', size=64, required=False, readonly=True, states={'draft': [('readonly', False)]}),
266         'employee_id': fields.many2one('hr.employee', 'Employee', required=True, readonly=True, states={'draft': [('readonly', False)]}),
267         'date_from': fields.date('Date From', readonly=True, states={'draft': [('readonly', False)]}, required=True),
268         'date_to': fields.date('Date To', readonly=True, states={'draft': [('readonly', False)]}, required=True),
269         'state': fields.selection([
270             ('draft', 'Draft'),
271             ('verify', 'Waiting'),
272             ('done', 'Done'),
273             ('cancel', 'Rejected'),
274         ], 'State', select=True, readonly=True,
275             help='* When the payslip is created the state is \'Draft\'.\
276             \n* If the payslip is under verification, the state is \'Waiting\'. \
277             \n* If the payslip is confirmed then state is set to \'Done\'.\
278             \n* When user cancel payslip the state is \'Rejected\'.'),
279 #        'line_ids': fields.one2many('hr.payslip.line', 'slip_id', 'Payslip Line', required=False, readonly=True, states={'draft': [('readonly', False)]}),
280         'line_ids': one2many_mod2('hr.payslip.line', 'slip_id', 'Payslip Lines', readonly=True, states={'draft':[('readonly',False)]}),
281         'company_id': fields.many2one('res.company', 'Company', required=False, readonly=True, states={'draft': [('readonly', False)]}),
282         'worked_days_line_ids': fields.one2many('hr.payslip.worked_days', 'payslip_id', 'Payslip Worked Days', required=False, readonly=True, states={'draft': [('readonly', False)]}),
283         'input_line_ids': fields.one2many('hr.payslip.input', 'payslip_id', 'Payslip Inputs', required=False, readonly=True, states={'draft': [('readonly', False)]}),
284         'paid': fields.boolean('Made Payment Order ? ', required=False, readonly=True, states={'draft': [('readonly', False)]}),
285         'note': fields.text('Description', readonly=True, states={'draft':[('readonly',False)]}),
286         'contract_id': fields.many2one('hr.contract', 'Contract', required=False, readonly=True, states={'draft': [('readonly', False)]}),
287         'details_by_salary_rule_category': fields.function(_get_lines_salary_rule_category, method=True, type='one2many', relation='hr.payslip.line', string='Details by Salary Rule Category'),
288         'credit_note': fields.boolean('Credit Note', help="Indicates this payslip has a refund of another", readonly=True, states={'draft': [('readonly', False)]}),
289         'payslip_run_id': fields.many2one('hr.payslip.run', 'Payslip Run', readonly=True, states={'draft': [('readonly', False)]}),
290     }
291     _defaults = {
292         'date_from': lambda *a: time.strftime('%Y-%m-01'),
293         'date_to': lambda *a: str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10],
294         'state': 'draft',
295         'credit_note': False,
296         'company_id': lambda self, cr, uid, context: \
297                 self.pool.get('res.users').browse(cr, uid, uid,
298                     context=context).company_id.id,
299     }
300
301     def copy(self, cr, uid, id, default=None, context=None):
302         if not default:
303             default = {}
304         company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
305         default.update({
306             'line_ids': [],
307             'move_ids': [],
308             'move_line_ids': [],
309             'company_id': company_id,
310             'period_id': False,
311             'basic_before_leaves': 0.0,
312             'basic_amount': 0.0
313         })
314         return super(hr_payslip, self).copy(cr, uid, id, default, context=context)
315
316     def cancel_sheet(self, cr, uid, ids, context=None):
317         return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
318
319     def process_sheet(self, cr, uid, ids, context=None):
320         return self.write(cr, uid, ids, {'paid': True, 'state': 'done'}, context=context)
321
322     def hr_verify_sheet(self, cr, uid, ids, context=None):
323         return self.write(cr, uid, ids, {'state': 'verify'}, context=context)
324
325     def refund_sheet(self, cr, uid, ids, context=None):
326         mod_obj = self.pool.get('ir.model.data')
327         wf_service = netsvc.LocalService("workflow")
328         for payslip in self.browse(cr, uid, ids, context=context):
329             id_copy = self.copy(cr, uid, payslip.id, {'credit_note': True, 'name': _('Refund: ')+payslip.name}, context=context)
330             self.compute_sheet(cr, uid, [id_copy], context=context)
331             wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'hr_verify_sheet', cr)
332             wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'process_sheet', cr)
333
334         form_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_form')
335         form_res = form_id and form_id[1] or False
336         tree_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_tree')
337         tree_res = tree_id and tree_id[1] or False
338         return {
339             'name':_("Refund Payslip"),
340             'view_mode': 'tree, form',
341             'view_id': False,
342             'view_type': 'form',
343             'res_model': 'hr.payslip',
344             'type': 'ir.actions.act_window',
345             'nodestroy': True,
346             'target': 'current',
347             'domain': "[('id', 'in', %s)]" % [id_copy],
348             'views': [(tree_res, 'tree'), (form_res, 'form')],
349             'context': {}
350         }
351
352     def check_done(self, cr, uid, ids, context=None):
353         return True
354
355     #TODO move this function into hr_contract module, on hr.employee object
356     def get_contract(self, cr, uid, employee, date_from, date_to, context=None):
357         """
358         @param employee: browse record of employee
359         @param date_from: date field
360         @param date_to: date field
361         @return: returns the ids of all the contracts for the given employee that need to be considered for the given dates
362         """
363         contract_obj = self.pool.get('hr.contract')
364         clause = []
365         #a contract is valid if it ends between the given dates
366         clause_1 = ['&',('date_end', '<=', date_to),('date_end','>=', date_from)]
367         #OR if it starts between the given dates
368         clause_2 = ['&',('date_start', '<=', date_to),('date_start','>=', date_from)]
369         #OR if it starts before the date_from and finish after the date_end (or never finish)
370         clause_3 = [('date_start','<=', date_from),'|',('date_end', '=', False),('date_end','>=', date_to)]
371         clause_final =  [('employee_id', '=', employee.id),'|','|'] + clause_1 + clause_2 + clause_3
372         contract_ids = contract_obj.search(cr, uid, clause_final, context=context)
373         return contract_ids
374
375     def compute_sheet(self, cr, uid, ids, context=None):
376         slip_line_pool = self.pool.get('hr.payslip.line')
377         sequence_obj = self.pool.get('ir.sequence')
378         for payslip in self.browse(cr, uid, ids, context=context):
379             number = payslip.number or sequence_obj.get(cr, uid, 'salary.slip')
380             #delete old payslip lines
381             old_slipline_ids = slip_line_pool.search(cr, uid, [('slip_id', '=', payslip.id)], context=context)
382 #            old_slipline_ids
383             if old_slipline_ids:
384                 slip_line_pool.unlink(cr, uid, old_slipline_ids, context=context)
385             if payslip.contract_id:
386                 #set the list of contract for which the rules have to be applied
387                 contract_ids = [payslip.contract_id.id]
388             else:
389                 #if we don't give the contract, then the rules to apply should be for all current contracts of the employee
390                 contract_ids = self.get_contract(cr, uid, payslip.employee_id, payslip.date_from, payslip.date_to, context=context)
391             lines = [(0,0,line) for line in self.pool.get('hr.payslip').get_payslip_lines(cr, uid, contract_ids, payslip.id, context=context)]
392             self.write(cr, uid, [payslip.id], {'line_ids': lines, 'number': number,}, context=context)
393         return True
394
395     def get_worked_day_lines(self, cr, uid, contract_ids, date_from, date_to, context=None):
396         """
397         @param contract_ids: list of contract id
398         @return: returns a list of dict containing the input that should be applied for the given contract between date_from and date_to
399         """
400         def was_on_leave(employee_id, datetime_day, context=None):
401             res = False
402             day = datetime_day.strftime("%Y-%m-%d")
403             holiday_ids = self.pool.get('hr.holidays').search(cr, uid, [('state','=','validate'),('employee_id','=',employee_id),('type','=','remove'),('date_from','<=',day),('date_to','>=',day)])
404             if holiday_ids:
405                 res = self.pool.get('hr.holidays').browse(cr, uid, holiday_ids, context=context)[0].holiday_status_id.name
406             return res
407
408         res = []
409         for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
410             if not contract.working_hours:
411                 #fill only if the contract as a working schedule linked
412                 continue
413             attendances = {
414                  'name': _("Normal Working Days paid at 100%"),
415                  'sequence': 1,
416                  'code': 'WORK100',
417                  'number_of_days': 0.0,
418                  'number_of_hours': 0.0,
419                  'contract_id': contract.id,
420             }
421             leaves = {}
422             day_from = datetime.strptime(date_from,"%Y-%m-%d")
423             day_to = datetime.strptime(date_to,"%Y-%m-%d")
424             nb_of_days = (day_to - day_from).days + 1
425             for day in range(0, nb_of_days):
426                 working_hours_on_day = self.pool.get('resource.calendar').working_hours_on_day(cr, uid, contract.working_hours, day_from + timedelta(days=day), context)
427                 if working_hours_on_day:
428                     #the employee had to work
429                     leave_type = was_on_leave(contract.employee_id.id, day_from + timedelta(days=day), context=context)
430                     if leave_type:
431                         #if he was on leave, fill the leaves dict
432                         if leave_type in leaves:
433                             leaves[leave_type]['number_of_days'] += 1.0
434                             leaves[leave_type]['number_of_hours'] += working_hours_on_day
435                         else:
436                             leaves[leave_type] = {
437                                 'name': leave_type,
438                                 'sequence': 5,
439                                 'code': leave_type,
440                                 'number_of_days': 1.0,
441                                 'number_of_hours': working_hours_on_day,
442                                 'contract_id': contract.id,
443                             }
444                     else:
445                         #add the input vals to tmp (increment if existing)
446                         attendances['number_of_days'] += 1.0
447                         attendances['number_of_hours'] += working_hours_on_day
448             leaves = [value for key,value in leaves.items()]
449             res += [attendances] + leaves
450         return res
451
452     def get_inputs(self, cr, uid, contract_ids, date_from, date_to, context=None):
453         res = []
454         contract_obj = self.pool.get('hr.contract')
455         rule_obj = self.pool.get('hr.salary.rule')
456
457         structure_ids = contract_obj.get_all_structures(cr, uid, contract_ids, context=context)
458         rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
459         sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
460
461         for contract in contract_obj.browse(cr, uid, contract_ids, context=context):
462             for rule in rule_obj.browse(cr, uid, sorted_rule_ids, context=context):
463                 if rule.input_ids:
464                     for input in rule.input_ids:
465                         inputs = {
466                              'name': input.name,
467                              'code': input.code,
468                              'contract_id': contract.id,
469                         }
470                         res += [inputs]
471         return res
472
473     def get_payslip_lines(self, cr, uid, contract_ids, payslip_id, context):
474         def _sum_salary_rule_category(localdict, category, amount):
475             if category.parent_id:
476                 localdict = _sum_salary_rule_category(localdict, category.parent_id, amount)
477             localdict['categories'].dict[category.code] = category.code in localdict['categories'].dict and localdict['categories'].dict[category.code] + amount or amount
478             return localdict
479
480         class BrowsableObject(object):
481             def __init__(self, pool, cr, uid, employee_id, dict):
482                 self.pool = pool
483                 self.cr = cr
484                 self.uid = uid
485                 self.employee_id = employee_id
486                 self.dict = dict
487
488             def __getattr__(self, attr):
489                 return attr in self.dict and self.dict.__getitem__(attr) or 0.0
490
491         class InputLine(BrowsableObject):
492             """a class that will be used into the python code, mainly for usability purposes"""
493             def sum(self, code, from_date, to_date=None):
494                 if to_date is None:
495                     to_date = datetime.now().strftime('%Y-%m-%d')
496                 result = 0.0
497                 self.cr.execute("SELECT sum(amount) as sum\
498                             FROM hr_payslip as hp, hr_payslip_input as pi \
499                             WHERE hp.employee_id = %s AND hp.state = 'done' \
500                             AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s",
501                            (self.employee_id, from_date, to_date, code))
502                 res = self.cr.fetchone()[0]
503                 return res or 0.0
504
505         class WorkedDays(BrowsableObject):
506             """a class that will be used into the python code, mainly for usability purposes"""
507             def _sum(self, code, from_date, to_date=None):
508                 if to_date is None:
509                     to_date = datetime.now().strftime('%Y-%m-%d')
510                 result = 0.0
511                 self.cr.execute("SELECT sum(number_of_days) as number_of_days, sum(number_of_hours) as number_of_hours\
512                             FROM hr_payslip as hp, hr_payslip_worked_days as pi \
513                             WHERE hp.employee_id = %s AND hp.state = 'done'\
514                             AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s",
515                            (self.employee_id, from_date, to_date, code))
516                 return self.cr.fetchone()
517
518             def sum(self, code, from_date, to_date=None):
519                 res = self._sum(code, from_date, to_date)
520                 return res and res[0] or 0.0
521
522             def sum_hours(self, code, from_date, to_date=None):
523                 res = self._sum(code, from_date, to_date)
524                 return res and res[1] or 0.0
525
526         class Payslips(BrowsableObject):
527             """a class that will be used into the python code, mainly for usability purposes"""
528
529             def sum(self, code, from_date, to_date=None):
530                 if to_date is None:
531                     to_date = datetime.now().strftime('%Y-%m-%d')
532                 self.cr.execute("SELECT sum(case when hp.credit_note = False then (pl.total) else (-pl.total) end)\
533                             FROM hr_payslip as hp, hr_payslip_line as pl \
534                             WHERE hp.employee_id = %s AND hp.state = 'done' \
535                             AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id AND pl.code = %s",
536                             (self.employee_id, from_date, to_date, code))
537                 res = self.cr.fetchone()
538                 return res and res[0] or 0.0
539
540         #we keep a dict with the result because a value can be overwritten by another rule with the same code
541         result_dict = {}
542         rules = {}
543         categories_dict = {}
544         blacklist = []
545         payslip_obj = self.pool.get('hr.payslip')
546         inputs_obj = self.pool.get('hr.payslip.worked_days')
547         obj_rule = self.pool.get('hr.salary.rule')
548         payslip = payslip_obj.browse(cr, uid, payslip_id, context=context)
549         worked_days = {}
550         for worked_days_line in payslip.worked_days_line_ids:
551             worked_days[worked_days_line.code] = worked_days_line
552         inputs = {}
553         for input_line in payslip.input_line_ids:
554             inputs[input_line.code] = input_line
555
556         categories_obj = BrowsableObject(self.pool, cr, uid, payslip.employee_id.id, categories_dict)
557         input_obj = InputLine(self.pool, cr, uid, payslip.employee_id.id, inputs)
558         worked_days_obj = WorkedDays(self.pool, cr, uid, payslip.employee_id.id, worked_days)
559         payslip_obj = Payslips(self.pool, cr, uid, payslip.employee_id.id, payslip)
560         rules_obj = BrowsableObject(self.pool, cr, uid, payslip.employee_id.id, rules)
561
562         localdict = {'categories': categories_obj, 'rules': rules_obj, 'payslip': payslip_obj, 'worked_days': worked_days_obj, 'inputs': input_obj}
563         #get the ids of the structures on the contracts and their parent id as well
564         structure_ids = self.pool.get('hr.contract').get_all_structures(cr, uid, contract_ids, context=context)
565         #get the rules of the structure and thier children
566         rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
567         #run the rules by sequence
568         sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
569
570         for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
571             employee = contract.employee_id
572             localdict.update({'employee': employee, 'contract': contract})
573             for rule in obj_rule.browse(cr, uid, sorted_rule_ids, context=context):
574                 key = rule.code + '-' + str(contract.id)
575                 localdict['result'] = None
576                 localdict['result_qty'] = 1.0
577                 #check if the rule can be applied
578                 if obj_rule.satisfy_condition(cr, uid, rule.id, localdict, context=context) and rule.id not in blacklist:
579                     #compute the amount of the rule
580                     amount, qty = obj_rule.compute_rule(cr, uid, rule.id, localdict, context=context)
581                     #check if there is already a rule computed with that code
582                     previous_amount = rule.code in localdict and localdict[rule.code] or 0.0
583                     #set/overwrite the amount computed for this rule in the localdict
584                     localdict[rule.code] = amount * qty
585                     rules[rule.code] = rule
586                     #sum the amount for its salary category
587                     localdict = _sum_salary_rule_category(localdict, rule.category_id, (amount * qty) - previous_amount)
588                     #create/overwrite the rule in the temporary results
589                     result_dict[key] = {
590                         'salary_rule_id': rule.id,
591                         'contract_id': contract.id,
592                         'name': rule.name,
593                         'code': rule.code,
594                         'category_id': rule.category_id.id,
595                         'sequence': rule.sequence,
596                         'appears_on_payslip': rule.appears_on_payslip,
597                         'condition_select': rule.condition_select,
598                         'condition_python': rule.condition_python,
599                         'condition_range': rule.condition_range,
600                         'condition_range_min': rule.condition_range_min,
601                         'condition_range_max': rule.condition_range_max,
602                         'amount_select': rule.amount_select,
603                         'amount_fix': rule.amount_fix,
604                         'amount_python_compute': rule.amount_python_compute,
605                         'amount_percentage': rule.amount_percentage,
606                         'amount_percentage_base': rule.amount_percentage_base,
607                         'register_id': rule.register_id.id,
608                         'amount': amount,
609                         'employee_id': contract.employee_id.id,
610                         'quantity': qty,
611                     }
612                 else:
613                     #blacklist this rule and its children
614                     blacklist += [id for id, seq in self.pool.get('hr.salary.rule')._recursive_search_of_rules(cr, uid, [rule], context=context)]
615
616         result = [value for code, value in result_dict.items()]
617         return result
618
619     def onchange_employee_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
620         empolyee_obj = self.pool.get('hr.employee')
621         contract_obj = self.pool.get('hr.contract')
622         worked_days_obj = self.pool.get('hr.payslip.worked_days')
623         input_obj = self.pool.get('hr.payslip.input')
624
625         if context is None:
626             context = {}
627         #delete old worked days lines
628         old_worked_days_ids = ids and worked_days_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
629         if old_worked_days_ids:
630             worked_days_obj.unlink(cr, uid, old_worked_days_ids, context=context)
631
632         #delete old input lines
633         old_input_ids = ids and input_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
634         if old_input_ids:
635             input_obj.unlink(cr, uid, old_input_ids, context=context)
636
637
638         #defaults
639         res = {'value':{
640                       'line_ids':[],
641                       'input_line_ids': [],
642                       'worked_days_line_ids': [],
643                       #'details_by_salary_head':[], TODO put me back
644                       'name':'',
645                       'contract_id': False,
646                       'struct_id': False,
647                       }
648             }
649         if not employee_id:
650             return res
651         ttyme = datetime.fromtimestamp(time.mktime(time.strptime(date_from, "%Y-%m-%d")))
652         employee_id = empolyee_obj.browse(cr, uid, employee_id, context=context)
653         res['value'].update({
654                     'name': _('Salary Slip of %s for %s') % (employee_id.name, tools.ustr(ttyme.strftime('%B-%Y'))),
655                     'company_id': employee_id.company_id.id
656         })
657
658         if not context.get('contract', False):
659             #fill with the first contract of the employee
660             contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
661             res['value'].update({
662                         'struct_id': contract_ids and contract_obj.read(cr, uid, contract_ids[0], ['struct_id'], context=context)['struct_id'][0] or False,
663                         'contract_id': contract_ids and contract_ids[0] or False,
664             })
665         else:
666             if contract_id:
667                 #set the list of contract for which the input have to be filled
668                 contract_ids = [contract_id]
669                 #fill the structure with the one on the selected contract
670                 contract_record = contract_obj.browse(cr, uid, contract_id, context=context)
671                 res['value'].update({
672                             'struct_id': contract_record.struct_id.id,
673                             'contract_id': contract_id
674                 })
675             else:
676                 #if we don't give the contract, then the input to fill should be for all current contracts of the employee
677                 contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
678                 if not contract_ids:
679                     return res
680
681         #computation of the salary input
682         worked_days_line_ids = self.get_worked_day_lines(cr, uid, contract_ids, date_from, date_to, context=context)
683         input_line_ids = self.get_inputs(cr, uid, contract_ids, date_from, date_to, context=context)
684         res['value'].update({
685                     'worked_days_line_ids': worked_days_line_ids,
686                     'input_line_ids': input_line_ids,
687         })
688         return res
689
690     def onchange_contract_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
691 #TODO it seems to be the mess in the onchanges, we should have onchange_employee => onchange_contract => doing all the things
692         if context is None:
693             context = {}
694         res = {'value':{
695                  'line_ids': [],
696                  'name': '',
697                  }
698               }
699         context.update({'contract': True})
700         if not contract_id:
701             res['value'].update({'struct_id': False})
702         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)
703
704 hr_payslip()
705
706 class hr_payslip_worked_days(osv.osv):
707     '''
708     Payslip Worked Days
709     '''
710
711     _name = 'hr.payslip.worked_days'
712     _description = 'Payslip Worked Days'
713     _columns = {
714         'name': fields.char('Description', size=256, required=True),
715         'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True),
716         'sequence': fields.integer('Sequence', required=True,),
717         'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
718         'number_of_days': fields.float('Number of Days'),
719         'number_of_hours': fields.float('Number of Hours'),
720         'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
721     }
722     _order = 'payslip_id, sequence'
723     _defaults = {
724         'sequence': 10,
725     }
726 hr_payslip_worked_days()
727
728 class hr_payslip_input(osv.osv):
729     '''
730     Payslip Input
731     '''
732
733     _name = 'hr.payslip.input'
734     _description = 'Payslip Input'
735     _columns = {
736         'name': fields.char('Description', size=256, required=True),
737         'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True),
738         'sequence': fields.integer('Sequence', required=True,),
739         'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
740         'amount': fields.float('Amount', help="It is used in computation. For e.g. A rule for sales having 1% commission of basic salary for per product can defined in expression like result = inputs.SALEURO.amount * contract.wage*0.01."),
741         'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
742     }
743     _order = 'payslip_id, sequence'
744     _defaults = {
745         'sequence': 10,
746         'amount': 0.0,
747     }
748
749 hr_payslip_input()
750
751 class hr_salary_rule(osv.osv):
752
753     _name = 'hr.salary.rule'
754     _columns = {
755         'name':fields.char('Name', size=256, required=True, readonly=False),
756         'code':fields.char('Code', size=64, required=True, help="The code of salary rules can be used as reference in computation of other rules. In that case, it is case sensitive."),
757         'sequence': fields.integer('Sequence', required=True, help='Use to arrange calculation sequence'),
758         '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."),
759         'category_id':fields.many2one('hr.salary.rule.category', 'Category', required=True),
760         '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."),
761         'appears_on_payslip': fields.boolean('Appears on Payslip', help="Used for the display of rule on payslip"),
762         'parent_rule_id':fields.many2one('hr.salary.rule', 'Parent Salary Rule', select=True),
763         'company_id':fields.many2one('res.company', 'Company', required=False),
764         'condition_select': fields.selection([('none', 'Always True'),('range', 'Range'), ('python', 'Python Expression')], "Condition Based on", required=True),
765         'condition_range':fields.char('Range Based on',size=1024, readonly=False, help='This will use to computer the % fields values, in general its on basic, but You can use all categories code field in small letter as a variable name i.e. hra, ma, lta, etc...., also you can use, static varible basic'),
766         '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
767         'condition_range_min': fields.float('Minimum Range', required=False, help="The minimum amount, applied for this rule."),
768         'condition_range_max': fields.float('Maximum Range', required=False, help="The maximum amount, applied for this rule."),
769         'amount_select':fields.selection([
770             ('percentage','Percentage (%)'),
771             ('fix','Fixed Amount'),
772             ('code','Python Code'),
773         ],'Amount Type', select=True, required=True, help="The computation method for the rule amount."),
774         'amount_fix': fields.float('Fixed Amount', digits_compute=dp.get_precision('Payroll'),),
775         'amount_percentage': fields.float('Percentage (%)', digits_compute=dp.get_precision('Payroll'), help='For example, enter 50.0 to apply a percentage of 50%'),
776         'amount_python_compute':fields.text('Python Code'),
777         'amount_percentage_base':fields.char('Percentage based on',size=1024, required=False, readonly=False, help='result will be affected to a variable'),
778         'child_ids':fields.one2many('hr.salary.rule', 'parent_rule_id', 'Child Salary Rule'),
779         'register_id':fields.many2one('hr.contribution.register', 'Contribution Register', help="Eventual third party involved in the salary payment of the employees."),
780         'input_ids': fields.one2many('hr.rule.input', 'input_id', 'Inputs'),
781         'note':fields.text('Description'),
782      }
783     _defaults = {
784         'amount_python_compute': '''
785 # Available variables:
786 #----------------------
787 # payslip: object containing the payslips
788 # employee: hr.employee object
789 # contract: hr.contract object
790 # rules: object containing the rules code (previously computed)
791 # categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category).
792 # worked_days: object containing the computed worked days.
793 # inputs: object containing the computed inputs.
794
795 # Note: returned value have to be set in the variable 'result'
796
797 result = contract.wage * 0.10''',
798         'condition_python':
799 '''
800 # Available variables:
801 #----------------------
802 # payslip: object containing the payslips
803 # employee: hr.employee object
804 # contract: hr.contract object
805 # rules: object containing the rules code (previously computed)
806 # categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category).
807 # worked_days: object containing the computed worked days
808 # inputs: object containing the computed inputs
809
810 # Note: returned value have to be set in the variable 'result'
811
812 result = rules.NET > categories.NET * 0.10''',
813         'condition_range': 'contract.wage',
814         'sequence': 5,
815         'appears_on_payslip': True,
816         'active': True,
817         'company_id': lambda self, cr, uid, context: \
818                 self.pool.get('res.users').browse(cr, uid, uid,
819                     context=context).company_id.id,
820         'condition_select': 'none',
821         'amount_select': 'fix',
822         'amount_fix': 0.0,
823         'amount_percentage': 0.0,
824         'quantity': '1.0',
825      }
826     
827     def _recursive_search_of_rules(self, cr, uid, rule_ids, context=None):
828         """
829         @param rule_ids: list of browse record
830         @return: returns a list of tuple (id, sequence) which are all the children of the passed rule_ids
831         """
832         children_rules = []
833         for rule in rule_ids:
834             if rule.child_ids:
835                 children_rules += self._recursive_search_of_rules(cr, uid, rule.child_ids, context=context)
836         return [(r.id, r.sequence) for r in rule_ids] + children_rules
837
838     #TODO should add some checks on the type of result (should be float)
839     def compute_rule(self, cr, uid, rule_id, localdict, context=None):
840         """
841         @param rule_id: id of rule to compute
842         @param localdict: dictionary containing the environement in which to compute the rule
843         @return: returns the result of computation and the quantity as floats
844         """
845         rule = self.browse(cr, uid, rule_id, context=context)
846         if rule.amount_select == 'fix':
847             try:
848                 return rule.amount_fix, eval(rule.quantity, localdict)
849             except:
850                 raise osv.except_osv(_('Error'), _('Wrong quantity defined for salary rule %s (%s)')% (rule.name, rule.code))
851         elif rule.amount_select == 'percentage':
852             try:
853                 amount = rule.amount_percentage * eval(rule.amount_percentage_base, localdict) / 100
854                 return amount, eval(rule.quantity, localdict)
855             except:
856                 raise osv.except_osv(_('Error'), _('Wrong percentage base or quantity defined for salary rule %s (%s)')% (rule.name, rule.code))
857         else:
858             try:
859                 eval(rule.amount_python_compute, localdict, mode='exec', nocopy=True)
860                 return localdict['result'], 'result_qty' in localdict and localdict['result_qty'] or 1.0
861             except:
862                 raise osv.except_osv(_('Error'), _('Wrong python code defined for salary rule %s (%s) ')% (rule.name, rule.code))
863
864     def satisfy_condition(self, cr, uid, rule_id, localdict, context=None):
865         """
866         @param rule_id: id of hr.salary.rule to be tested
867         @param contract_id: id of hr.contract to be tested
868         @return: returns True if the given rule match the condition for the given contract. Return False otherwise.
869         """
870         rule = self.browse(cr, uid, rule_id, context=context)
871
872         if rule.condition_select == 'none':
873             return True
874         elif rule.condition_select == 'range':
875             try:
876                 result = eval(rule.condition_range, localdict)
877                 return rule.condition_range_min <=  result and result <= rule.condition_range_max or False
878             except:
879                 raise osv.except_osv(_('Error'), _('Wrong range condition defined for salary rule %s (%s)')% (rule.name, rule.code))
880         else: #python code
881             try:
882                 eval(rule.condition_python, localdict, mode='exec', nocopy=True)
883                 return 'result' in localdict and localdict['result'] or False
884             except:
885                 raise osv.except_osv(_('Error'), _('Wrong python condition defined for salary rule %s (%s)')% (rule.name, rule.code))
886
887 hr_salary_rule()
888
889 class hr_rule_input(osv.osv):
890     '''
891     Salary Rule Input
892     '''
893
894     _name = 'hr.rule.input'
895     _description = 'Salary Rule Input'
896     _columns = {
897         'name': fields.char('Description', size=256, required=True),
898         'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
899         'input_id': fields.many2one('hr.salary.rule', 'Salary Rule Input', required=True)
900     }
901
902 hr_rule_input()
903
904 class hr_payslip_line(osv.osv):
905     '''
906     Payslip Line
907     '''
908
909     _name = 'hr.payslip.line'
910     _inherit = 'hr.salary.rule'
911     _description = 'Payslip Line'
912     _order = 'contract_id, sequence'
913
914     def _calculate_total(self, cr, uid, ids, name, args, context):
915         if not ids: return {}
916         res = {}
917         for line in self.browse(cr, uid, ids, context=context):
918             res[line.id] = float(line.quantity) * line.amount
919         return res
920
921     _columns = {
922         'slip_id':fields.many2one('hr.payslip', 'Pay Slip', required=True),
923         'salary_rule_id':fields.many2one('hr.salary.rule', 'Rule', required=True),
924         'employee_id':fields.many2one('hr.employee', 'Employee', required=True),
925         'contract_id':fields.many2one('hr.contract', 'Contract', required=True),
926         'amount': fields.float('Amount', digits_compute=dp.get_precision('Payroll')),
927         'quantity': fields.float('Quantity', digits_compute=dp.get_precision('Payroll')),
928         'total': fields.function(_calculate_total, method=True, type='float', string='Total', digits_compute=dp.get_precision('Payroll'),store=True ),
929     }
930
931 hr_payslip_line()
932
933 class hr_payroll_structure(osv.osv):
934
935     _inherit = 'hr.payroll.structure'
936     _columns = {
937         'rule_ids':fields.many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id', 'Salary Rules'),
938     }
939
940 hr_payroll_structure()
941
942 class hr_employee(osv.osv):
943     '''
944     Employee
945     '''
946
947     _inherit = 'hr.employee'
948     _description = 'Employee'
949
950     def _calculate_total_wage(self, cr, uid, ids, name, args, context):
951         if not ids: return {}
952         res = {}
953         current_date = datetime.now().strftime('%Y-%m-%d')
954         for employee in self.browse(cr, uid, ids, context=context):
955             if not employee.contract_ids:
956                 res[employee.id] = {'basic': 0.0}
957                 continue
958             cr.execute( 'SELECT SUM(wage) '\
959                         'FROM hr_contract '\
960                         'WHERE employee_id = %s '\
961                         'AND date_start <= %s '\
962                         'AND (date_end > %s OR date_end is NULL)',
963                          (employee.id, current_date, current_date))
964             result = dict(cr.dictfetchone())
965             res[employee.id] = {'basic': result['sum']}
966         return res
967
968     _columns = {
969         'slip_ids':fields.one2many('hr.payslip', 'employee_id', 'Payslips', required=False, readonly=True),
970         'total_wage': fields.function(_calculate_total_wage, method=True, type='float', string='Total Basic Salary', digits_compute=dp.get_precision('Payroll'), help="Sum of all current contract's wage of employee."),
971     }
972
973 hr_employee()
974
975 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: