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