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