[FIX] sent mail, quote template controller name
[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         structure_ids = [contract.struct_id.id for contract in self.browse(cr, uid, contract_ids, context=context) if contract.struct_id]
147         if not structure_ids:
148             return []
149         return list(set(self.pool.get('hr.payroll.structure')._get_parent_structure(cr, uid, structure_ids, context=context)))
150
151
152 class contrib_register(osv.osv):
153     '''
154     Contribution Register
155     '''
156
157     _name = 'hr.contribution.register'
158     _description = 'Contribution Register'
159
160     _columns = {
161         'company_id':fields.many2one('res.company', 'Company'),
162         'partner_id':fields.many2one('res.partner', 'Partner'),
163         'name':fields.char('Name', size=256, required=True, readonly=False),
164         'register_line_ids':fields.one2many('hr.payslip.line', 'register_id', 'Register Line', readonly=True),
165         'note': fields.text('Description'),
166     }
167     _defaults = {
168         'company_id': lambda self, cr, uid, context: \
169                 self.pool.get('res.users').browse(cr, uid, uid,
170                     context=context).company_id.id,
171     }
172
173
174 class hr_salary_rule_category(osv.osv):
175     """
176     HR Salary Rule Category
177     """
178
179     _name = 'hr.salary.rule.category'
180     _description = 'Salary Rule Category'
181     _columns = {
182         'name':fields.char('Name', size=64, required=True, readonly=False),
183         'code':fields.char('Code', size=64, required=True, readonly=False),
184         'parent_id':fields.many2one('hr.salary.rule.category', 'Parent', help="Linking a salary category to its parent is used only for the reporting purpose."),
185         'children_ids': fields.one2many('hr.salary.rule.category', 'parent_id', 'Children'),
186         'note': fields.text('Description'),
187         'company_id':fields.many2one('res.company', 'Company', required=False),
188     }
189
190     _defaults = {
191         'company_id': lambda self, cr, uid, context: \
192                 self.pool.get('res.users').browse(cr, uid, uid,
193                     context=context).company_id.id,
194     }
195
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[self._obj].search(cr, user, [(self._fields_id,'in',ids), ('appears_on_payslip', '=', True)], limit=self._limit)
208         for r in obj.pool[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
240 class hr_payslip(osv.osv):
241     '''
242     Pay Slip
243     '''
244
245     _name = 'hr.payslip'
246     _description = 'Pay Slip'
247
248     def _get_lines_salary_rule_category(self, cr, uid, ids, field_names, arg=None, context=None):
249         result = {}
250         if not ids: return result
251         for id in ids:
252             result.setdefault(id, [])
253         cr.execute('''SELECT pl.slip_id, pl.id FROM hr_payslip_line AS pl \
254                     LEFT JOIN hr_salary_rule_category AS sh on (pl.category_id = sh.id) \
255                     WHERE pl.slip_id in %s \
256                     GROUP BY pl.slip_id, pl.sequence, pl.id ORDER BY pl.sequence''',(tuple(ids),))
257         res = cr.fetchall()
258         for r in res:
259             result[r[0]].append(r[1])
260         return result
261
262     _columns = {
263         'struct_id': fields.many2one('hr.payroll.structure', 'Structure', readonly=True, states={'draft': [('readonly', False)]}, help='Defines the rules that have to be applied to this payslip, accordingly to the contract chosen. If you let empty the field contract, this field isn\'t mandatory anymore and thus the rules applied will be all the rules set on the structure of all contracts of the employee valid for the chosen period'),
264         'name': fields.char('Payslip Name', size=64, required=False, readonly=True, states={'draft': [('readonly', False)]}),
265         'number': fields.char('Reference', size=64, required=False, readonly=True, states={'draft': [('readonly', False)]}),
266         'employee_id': fields.many2one('hr.employee', 'Employee', required=True, readonly=True, states={'draft': [('readonly', False)]}),
267         'date_from': fields.date('Date From', readonly=True, states={'draft': [('readonly', False)]}, required=True),
268         'date_to': fields.date('Date To', readonly=True, states={'draft': [('readonly', False)]}, required=True),
269         'state': fields.selection([
270             ('draft', 'Draft'),
271             ('verify', 'Waiting'),
272             ('done', 'Done'),
273             ('cancel', 'Rejected'),
274         ], 'Status', select=True, readonly=True,
275             help='* When the payslip is created the status is \'Draft\'.\
276             \n* If the payslip is under verification, the status is \'Waiting\'. \
277             \n* If the payslip is confirmed then status is set to \'Done\'.\
278             \n* When user cancel payslip the status is \'Rejected\'.'),
279 #        'line_ids': fields.one2many('hr.payslip.line', 'slip_id', 'Payslip Line', required=False, readonly=True, states={'draft': [('readonly', False)]}),
280         'line_ids': one2many_mod2('hr.payslip.line', 'slip_id', 'Payslip Lines', readonly=True, states={'draft':[('readonly',False)]}),
281         'company_id': fields.many2one('res.company', 'Company', required=False, readonly=True, states={'draft': [('readonly', False)]}),
282         'worked_days_line_ids': fields.one2many('hr.payslip.worked_days', 'payslip_id', 'Payslip Worked Days', required=False, readonly=True, states={'draft': [('readonly', False)]}),
283         'input_line_ids': fields.one2many('hr.payslip.input', 'payslip_id', 'Payslip Inputs', required=False, readonly=True, states={'draft': [('readonly', False)]}),
284         'paid': fields.boolean('Made Payment Order ? ', required=False, readonly=True, states={'draft': [('readonly', False)]}),
285         'note': fields.text('Internal Note', readonly=True, states={'draft':[('readonly',False)]}),
286         'contract_id': fields.many2one('hr.contract', 'Contract', required=False, readonly=True, states={'draft': [('readonly', False)]}),
287         'details_by_salary_rule_category': fields.function(_get_lines_salary_rule_category, method=True, type='one2many', relation='hr.payslip.line', string='Details by Salary Rule Category'),
288         'credit_note': fields.boolean('Credit Note', help="Indicates this payslip has a refund of another", readonly=True, states={'draft': [('readonly', False)]}),
289         'payslip_run_id': fields.many2one('hr.payslip.run', 'Payslip Batches', readonly=True, states={'draft': [('readonly', False)]}),
290     }
291     _defaults = {
292         'date_from': lambda *a: time.strftime('%Y-%m-01'),
293         'date_to': lambda *a: str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10],
294         'state': 'draft',
295         'credit_note': False,
296         'company_id': lambda self, cr, uid, context: \
297                 self.pool.get('res.users').browse(cr, uid, uid,
298                     context=context).company_id.id,
299     }
300
301     def _check_dates(self, cr, uid, ids, context=None):
302         for payslip in self.browse(cr, uid, ids, context=context):
303             if payslip.date_from > payslip.date_to:
304                 return False
305         return True
306
307     _constraints = [(_check_dates, "Payslip 'Date From' must be before 'Date To'.", ['date_from', 'date_to'])]
308
309     def copy(self, cr, uid, id, default=None, context=None):
310         if not default:
311             default = {}
312         company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
313         default.update({
314             'line_ids': [],
315             'company_id': company_id,
316             'number': '',
317             'payslip_run_id': False,
318             'paid': False,
319         })
320         return super(hr_payslip, self).copy(cr, uid, id, default, context=context)
321
322     def cancel_sheet(self, cr, uid, ids, context=None):
323         return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
324
325     def process_sheet(self, cr, uid, ids, context=None):
326         return self.write(cr, uid, ids, {'paid': True, 'state': 'done'}, context=context)
327
328     def hr_verify_sheet(self, cr, uid, ids, context=None):
329         self.compute_sheet(cr, uid, ids, context)
330         return self.write(cr, uid, ids, {'state': 'verify'}, context=context)
331
332     def refund_sheet(self, cr, uid, ids, context=None):
333         mod_obj = self.pool.get('ir.model.data')
334         for payslip in self.browse(cr, uid, ids, context=context):
335             id_copy = self.copy(cr, uid, payslip.id, {'credit_note': True, 'name': _('Refund: ')+payslip.name}, context=context)
336             self.compute_sheet(cr, uid, [id_copy], context=context)
337             self.signal_hr_verify_sheet(cr, uid, [id_copy])
338             self.signal_process_sheet(cr, uid, [id_copy])
339             
340         form_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_form')
341         form_res = form_id and form_id[1] or False
342         tree_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_tree')
343         tree_res = tree_id and tree_id[1] or False
344         return {
345             'name':_("Refund Payslip"),
346             'view_mode': 'tree, form',
347             'view_id': False,
348             'view_type': 'form',
349             'res_model': 'hr.payslip',
350             'type': 'ir.actions.act_window',
351             'nodestroy': True,
352             'target': 'current',
353             'domain': "[('id', 'in', %s)]" % [id_copy],
354             'views': [(tree_res, 'tree'), (form_res, 'form')],
355             'context': {}
356         }
357
358     def check_done(self, cr, uid, ids, context=None):
359         return True
360
361     def unlink(self, cr, uid, ids, context=None):
362         for payslip in self.browse(cr, uid, ids, context=context):
363             if payslip.state not in  ['draft','cancel']:
364                 raise osv.except_osv(_('Warning!'),_('You cannot delete a payslip which is not draft or cancelled!'))
365         return super(hr_payslip, self).unlink(cr, uid, ids, context)
366
367     #TODO move this function into hr_contract module, on hr.employee object
368     def get_contract(self, cr, uid, employee, date_from, date_to, context=None):
369         """
370         @param employee: browse record of employee
371         @param date_from: date field
372         @param date_to: date field
373         @return: returns the ids of all the contracts for the given employee that need to be considered for the given dates
374         """
375         contract_obj = self.pool.get('hr.contract')
376         clause = []
377         #a contract is valid if it ends between the given dates
378         clause_1 = ['&',('date_end', '<=', date_to),('date_end','>=', date_from)]
379         #OR if it starts between the given dates
380         clause_2 = ['&',('date_start', '<=', date_to),('date_start','>=', date_from)]
381         #OR if it starts before the date_from and finish after the date_end (or never finish)
382         clause_3 = [('date_start','<=', date_from),'|',('date_end', '=', False),('date_end','>=', date_to)]
383         clause_final =  [('employee_id', '=', employee.id),'|','|'] + clause_1 + clause_2 + clause_3
384         contract_ids = contract_obj.search(cr, uid, clause_final, context=context)
385         return contract_ids
386
387     def compute_sheet(self, cr, uid, ids, context=None):
388         slip_line_pool = self.pool.get('hr.payslip.line')
389         sequence_obj = self.pool.get('ir.sequence')
390         for payslip in self.browse(cr, uid, ids, context=context):
391             number = payslip.number or sequence_obj.get(cr, uid, 'salary.slip')
392             #delete old payslip lines
393             old_slipline_ids = slip_line_pool.search(cr, uid, [('slip_id', '=', payslip.id)], context=context)
394 #            old_slipline_ids
395             if old_slipline_ids:
396                 slip_line_pool.unlink(cr, uid, old_slipline_ids, context=context)
397             if payslip.contract_id:
398                 #set the list of contract for which the rules have to be applied
399                 contract_ids = [payslip.contract_id.id]
400             else:
401                 #if we don't give the contract, then the rules to apply should be for all current contracts of the employee
402                 contract_ids = self.get_contract(cr, uid, payslip.employee_id, payslip.date_from, payslip.date_to, context=context)
403             lines = [(0,0,line) for line in self.pool.get('hr.payslip').get_payslip_lines(cr, uid, contract_ids, payslip.id, context=context)]
404             self.write(cr, uid, [payslip.id], {'line_ids': lines, 'number': number,}, context=context)
405         return True
406
407     def get_worked_day_lines(self, cr, uid, contract_ids, date_from, date_to, context=None):
408         """
409         @param contract_ids: list of contract id
410         @return: returns a list of dict containing the input that should be applied for the given contract between date_from and date_to
411         """
412         def was_on_leave(employee_id, datetime_day, context=None):
413             res = False
414             day = datetime_day.strftime("%Y-%m-%d")
415             holiday_ids = self.pool.get('hr.holidays').search(cr, uid, [('state','=','validate'),('employee_id','=',employee_id),('type','=','remove'),('date_from','<=',day),('date_to','>=',day)])
416             if holiday_ids:
417                 res = self.pool.get('hr.holidays').browse(cr, uid, holiday_ids, context=context)[0].holiday_status_id.name
418             return res
419
420         res = []
421         for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
422             if not contract.working_hours:
423                 #fill only if the contract as a working schedule linked
424                 continue
425             attendances = {
426                  'name': _("Normal Working Days paid at 100%"),
427                  'sequence': 1,
428                  'code': 'WORK100',
429                  'number_of_days': 0.0,
430                  'number_of_hours': 0.0,
431                  'contract_id': contract.id,
432             }
433             leaves = {}
434             day_from = datetime.strptime(date_from,"%Y-%m-%d")
435             day_to = datetime.strptime(date_to,"%Y-%m-%d")
436             nb_of_days = (day_to - day_from).days + 1
437             for day in range(0, nb_of_days):
438                 working_hours_on_day = self.pool.get('resource.calendar').working_hours_on_day(cr, uid, contract.working_hours, day_from + timedelta(days=day), context)
439                 if working_hours_on_day:
440                     #the employee had to work
441                     leave_type = was_on_leave(contract.employee_id.id, day_from + timedelta(days=day), context=context)
442                     if leave_type:
443                         #if he was on leave, fill the leaves dict
444                         if leave_type in leaves:
445                             leaves[leave_type]['number_of_days'] += 1.0
446                             leaves[leave_type]['number_of_hours'] += working_hours_on_day
447                         else:
448                             leaves[leave_type] = {
449                                 'name': leave_type,
450                                 'sequence': 5,
451                                 'code': leave_type,
452                                 'number_of_days': 1.0,
453                                 'number_of_hours': working_hours_on_day,
454                                 'contract_id': contract.id,
455                             }
456                     else:
457                         #add the input vals to tmp (increment if existing)
458                         attendances['number_of_days'] += 1.0
459                         attendances['number_of_hours'] += working_hours_on_day
460             leaves = [value for key,value in leaves.items()]
461             res += [attendances] + leaves
462         return res
463
464     def get_inputs(self, cr, uid, contract_ids, date_from, date_to, context=None):
465         res = []
466         contract_obj = self.pool.get('hr.contract')
467         rule_obj = self.pool.get('hr.salary.rule')
468
469         structure_ids = contract_obj.get_all_structures(cr, uid, contract_ids, context=context)
470         rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
471         sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
472
473         for contract in contract_obj.browse(cr, uid, contract_ids, context=context):
474             for rule in rule_obj.browse(cr, uid, sorted_rule_ids, context=context):
475                 if rule.input_ids:
476                     for input in rule.input_ids:
477                         inputs = {
478                              'name': input.name,
479                              'code': input.code,
480                              'contract_id': contract.id,
481                         }
482                         res += [inputs]
483         return res
484
485     def get_payslip_lines(self, cr, uid, contract_ids, payslip_id, context):
486         def _sum_salary_rule_category(localdict, category, amount):
487             if category.parent_id:
488                 localdict = _sum_salary_rule_category(localdict, category.parent_id, amount)
489             localdict['categories'].dict[category.code] = category.code in localdict['categories'].dict and localdict['categories'].dict[category.code] + amount or amount
490             return localdict
491
492         class BrowsableObject(object):
493             def __init__(self, pool, cr, uid, employee_id, dict):
494                 self.pool = pool
495                 self.cr = cr
496                 self.uid = uid
497                 self.employee_id = employee_id
498                 self.dict = dict
499
500             def __getattr__(self, attr):
501                 return attr in self.dict and self.dict.__getitem__(attr) or 0.0
502
503         class InputLine(BrowsableObject):
504             """a class that will be used into the python code, mainly for usability purposes"""
505             def sum(self, code, from_date, to_date=None):
506                 if to_date is None:
507                     to_date = datetime.now().strftime('%Y-%m-%d')
508                 result = 0.0
509                 self.cr.execute("SELECT sum(amount) as sum\
510                             FROM hr_payslip as hp, hr_payslip_input as pi \
511                             WHERE hp.employee_id = %s AND hp.state = 'done' \
512                             AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s",
513                            (self.employee_id, from_date, to_date, code))
514                 res = self.cr.fetchone()[0]
515                 return res or 0.0
516
517         class WorkedDays(BrowsableObject):
518             """a class that will be used into the python code, mainly for usability purposes"""
519             def _sum(self, code, from_date, to_date=None):
520                 if to_date is None:
521                     to_date = datetime.now().strftime('%Y-%m-%d')
522                 result = 0.0
523                 self.cr.execute("SELECT sum(number_of_days) as number_of_days, sum(number_of_hours) as number_of_hours\
524                             FROM hr_payslip as hp, hr_payslip_worked_days as pi \
525                             WHERE hp.employee_id = %s AND hp.state = 'done'\
526                             AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s",
527                            (self.employee_id, from_date, to_date, code))
528                 return self.cr.fetchone()
529
530             def sum(self, code, from_date, to_date=None):
531                 res = self._sum(code, from_date, to_date)
532                 return res and res[0] or 0.0
533
534             def sum_hours(self, code, from_date, to_date=None):
535                 res = self._sum(code, from_date, to_date)
536                 return res and res[1] or 0.0
537
538         class Payslips(BrowsableObject):
539             """a class that will be used into the python code, mainly for usability purposes"""
540
541             def sum(self, code, from_date, to_date=None):
542                 if to_date is None:
543                     to_date = datetime.now().strftime('%Y-%m-%d')
544                 self.cr.execute("SELECT sum(case when hp.credit_note = False then (pl.total) else (-pl.total) end)\
545                             FROM hr_payslip as hp, hr_payslip_line as pl \
546                             WHERE hp.employee_id = %s AND hp.state = 'done' \
547                             AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id AND pl.code = %s",
548                             (self.employee_id, from_date, to_date, code))
549                 res = self.cr.fetchone()
550                 return res and res[0] or 0.0
551
552         #we keep a dict with the result because a value can be overwritten by another rule with the same code
553         result_dict = {}
554         rules = {}
555         categories_dict = {}
556         blacklist = []
557         payslip_obj = self.pool.get('hr.payslip')
558         inputs_obj = self.pool.get('hr.payslip.worked_days')
559         obj_rule = self.pool.get('hr.salary.rule')
560         payslip = payslip_obj.browse(cr, uid, payslip_id, context=context)
561         worked_days = {}
562         for worked_days_line in payslip.worked_days_line_ids:
563             worked_days[worked_days_line.code] = worked_days_line
564         inputs = {}
565         for input_line in payslip.input_line_ids:
566             inputs[input_line.code] = input_line
567
568         categories_obj = BrowsableObject(self.pool, cr, uid, payslip.employee_id.id, categories_dict)
569         input_obj = InputLine(self.pool, cr, uid, payslip.employee_id.id, inputs)
570         worked_days_obj = WorkedDays(self.pool, cr, uid, payslip.employee_id.id, worked_days)
571         payslip_obj = Payslips(self.pool, cr, uid, payslip.employee_id.id, payslip)
572         rules_obj = BrowsableObject(self.pool, cr, uid, payslip.employee_id.id, rules)
573
574         localdict = {'categories': categories_obj, 'rules': rules_obj, 'payslip': payslip_obj, 'worked_days': worked_days_obj, 'inputs': input_obj}
575         #get the ids of the structures on the contracts and their parent id as well
576         structure_ids = self.pool.get('hr.contract').get_all_structures(cr, uid, contract_ids, context=context)
577         #get the rules of the structure and thier children
578         rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
579         #run the rules by sequence
580         sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
581
582         for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
583             employee = contract.employee_id
584             localdict.update({'employee': employee, 'contract': contract})
585             for rule in obj_rule.browse(cr, uid, sorted_rule_ids, context=context):
586                 key = rule.code + '-' + str(contract.id)
587                 localdict['result'] = None
588                 localdict['result_qty'] = 1.0
589                 #check if the rule can be applied
590                 if obj_rule.satisfy_condition(cr, uid, rule.id, localdict, context=context) and rule.id not in blacklist:
591                     #compute the amount of the rule
592                     amount, qty, rate = obj_rule.compute_rule(cr, uid, rule.id, localdict, context=context)
593                     #check if there is already a rule computed with that code
594                     previous_amount = rule.code in localdict and localdict[rule.code] or 0.0
595                     #set/overwrite the amount computed for this rule in the localdict
596                     tot_rule = amount * qty * rate / 100.0
597                     localdict[rule.code] = tot_rule
598                     rules[rule.code] = rule
599                     #sum the amount for its salary category
600                     localdict = _sum_salary_rule_category(localdict, rule.category_id, tot_rule - previous_amount)
601                     #create/overwrite the rule in the temporary results
602                     result_dict[key] = {
603                         'salary_rule_id': rule.id,
604                         'contract_id': contract.id,
605                         'name': rule.name,
606                         'code': rule.code,
607                         'category_id': rule.category_id.id,
608                         'sequence': rule.sequence,
609                         'appears_on_payslip': rule.appears_on_payslip,
610                         'condition_select': rule.condition_select,
611                         'condition_python': rule.condition_python,
612                         'condition_range': rule.condition_range,
613                         'condition_range_min': rule.condition_range_min,
614                         'condition_range_max': rule.condition_range_max,
615                         'amount_select': rule.amount_select,
616                         'amount_fix': rule.amount_fix,
617                         'amount_python_compute': rule.amount_python_compute,
618                         'amount_percentage': rule.amount_percentage,
619                         'amount_percentage_base': rule.amount_percentage_base,
620                         'register_id': rule.register_id.id,
621                         'amount': amount,
622                         'employee_id': contract.employee_id.id,
623                         'quantity': qty,
624                         'rate': rate,
625                     }
626                 else:
627                     #blacklist this rule and its children
628                     blacklist += [id for id, seq in self.pool.get('hr.salary.rule')._recursive_search_of_rules(cr, uid, [rule], context=context)]
629
630         result = [value for code, value in result_dict.items()]
631         return result
632
633     def onchange_employee_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
634         empolyee_obj = self.pool.get('hr.employee')
635         contract_obj = self.pool.get('hr.contract')
636         worked_days_obj = self.pool.get('hr.payslip.worked_days')
637         input_obj = self.pool.get('hr.payslip.input')
638
639         if context is None:
640             context = {}
641         #delete old worked days lines
642         old_worked_days_ids = ids and worked_days_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
643         if old_worked_days_ids:
644             worked_days_obj.unlink(cr, uid, old_worked_days_ids, context=context)
645
646         #delete old input lines
647         old_input_ids = ids and input_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
648         if old_input_ids:
649             input_obj.unlink(cr, uid, old_input_ids, context=context)
650
651
652         #defaults
653         res = {'value':{
654                       'line_ids':[],
655                       'input_line_ids': [],
656                       'worked_days_line_ids': [],
657                       #'details_by_salary_head':[], TODO put me back
658                       'name':'',
659                       'contract_id': False,
660                       'struct_id': False,
661                       }
662             }
663         if (not employee_id) or (not date_from) or (not date_to):
664             return res
665         ttyme = datetime.fromtimestamp(time.mktime(time.strptime(date_from, "%Y-%m-%d")))
666         employee_id = empolyee_obj.browse(cr, uid, employee_id, context=context)
667         res['value'].update({
668                     'name': _('Salary Slip of %s for %s') % (employee_id.name, tools.ustr(ttyme.strftime('%B-%Y'))),
669                     'company_id': employee_id.company_id.id
670         })
671
672         if not context.get('contract', False):
673             #fill with the first contract of the employee
674             contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
675         else:
676             if contract_id:
677                 #set the list of contract for which the input have to be filled
678                 contract_ids = [contract_id]
679             else:
680                 #if we don't give the contract, then the input to fill should be for all current contracts of the employee
681                 contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
682
683         if not contract_ids:
684             return res
685         contract_record = contract_obj.browse(cr, uid, contract_ids[0], context=context)
686         res['value'].update({
687                     'contract_id': contract_record and contract_record.id or False
688         })
689         struct_record = contract_record and contract_record.struct_id or False
690         if not struct_record:
691             return res
692         res['value'].update({
693                     'struct_id': struct_record.id,
694         })
695         #computation of the salary input
696         worked_days_line_ids = self.get_worked_day_lines(cr, uid, contract_ids, date_from, date_to, context=context)
697         input_line_ids = self.get_inputs(cr, uid, contract_ids, date_from, date_to, context=context)
698         res['value'].update({
699                     'worked_days_line_ids': worked_days_line_ids,
700                     'input_line_ids': input_line_ids,
701         })
702         return res
703
704     def onchange_contract_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
705 #TODO it seems to be the mess in the onchanges, we should have onchange_employee => onchange_contract => doing all the things
706         if context is None:
707             context = {}
708         res = {'value':{
709                  'line_ids': [],
710                  'name': '',
711                  }
712               }
713         context.update({'contract': True})
714         if not contract_id:
715             res['value'].update({'struct_id': False})
716         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)
717
718
719 class hr_payslip_worked_days(osv.osv):
720     '''
721     Payslip Worked Days
722     '''
723
724     _name = 'hr.payslip.worked_days'
725     _description = 'Payslip Worked Days'
726     _columns = {
727         'name': fields.char('Description', size=256, required=True),
728         'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade', select=True),
729         'sequence': fields.integer('Sequence', required=True, select=True),
730         'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
731         'number_of_days': fields.float('Number of Days'),
732         'number_of_hours': fields.float('Number of Hours'),
733         'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
734     }
735     _order = 'payslip_id, sequence'
736     _defaults = {
737         'sequence': 10,
738     }
739
740 class hr_payslip_input(osv.osv):
741     '''
742     Payslip Input
743     '''
744
745     _name = 'hr.payslip.input'
746     _description = 'Payslip Input'
747     _columns = {
748         'name': fields.char('Description', size=256, required=True),
749         'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade', select=True),
750         'sequence': fields.integer('Sequence', required=True, select=True),
751         'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
752         '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."),
753         'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
754     }
755     _order = 'payslip_id, sequence'
756     _defaults = {
757         'sequence': 10,
758         'amount': 0.0,
759     }
760
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
899 class hr_rule_input(osv.osv):
900     '''
901     Salary Rule Input
902     '''
903
904     _name = 'hr.rule.input'
905     _description = 'Salary Rule Input'
906     _columns = {
907         'name': fields.char('Description', size=256, required=True),
908         'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
909         'input_id': fields.many2one('hr.salary.rule', 'Salary Rule Input', required=True)
910     }
911
912
913 class hr_payslip_line(osv.osv):
914     '''
915     Payslip Line
916     '''
917
918     _name = 'hr.payslip.line'
919     _inherit = 'hr.salary.rule'
920     _description = 'Payslip Line'
921     _order = 'contract_id, sequence'
922
923     def _calculate_total(self, cr, uid, ids, name, args, context):
924         if not ids: return {}
925         res = {}
926         for line in self.browse(cr, uid, ids, context=context):
927             res[line.id] = float(line.quantity) * line.amount * line.rate / 100
928         return res
929
930     _columns = {
931         'slip_id':fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade'),
932         'salary_rule_id':fields.many2one('hr.salary.rule', 'Rule', required=True),
933         'employee_id':fields.many2one('hr.employee', 'Employee', required=True),
934         'contract_id':fields.many2one('hr.contract', 'Contract', required=True, select=True),
935         'rate': fields.float('Rate (%)', digits_compute=dp.get_precision('Payroll Rate')),
936         'amount': fields.float('Amount', digits_compute=dp.get_precision('Payroll')),
937         'quantity': fields.float('Quantity', digits_compute=dp.get_precision('Payroll')),
938         'total': fields.function(_calculate_total, method=True, type='float', string='Total', digits_compute=dp.get_precision('Payroll'),store=True ),
939     }
940
941     _defaults = {
942         'quantity': 1.0,
943         'rate': 100.0,
944     }
945
946
947
948 class hr_employee(osv.osv):
949     '''
950     Employee
951     '''
952
953     _inherit = 'hr.employee'
954     _description = 'Employee'
955
956     def _calculate_total_wage(self, cr, uid, ids, name, args, context):
957         if not ids: return {}
958         res = {}
959         current_date = datetime.now().strftime('%Y-%m-%d')
960         for employee in self.browse(cr, uid, ids, context=context):
961             if not employee.contract_ids:
962                 res[employee.id] = {'basic': 0.0}
963                 continue
964             cr.execute( 'SELECT SUM(wage) '\
965                         'FROM hr_contract '\
966                         'WHERE employee_id = %s '\
967                         'AND date_start <= %s '\
968                         'AND (date_end > %s OR date_end is NULL)',
969                          (employee.id, current_date, current_date))
970             result = dict(cr.dictfetchone())
971             res[employee.id] = {'basic': result['sum']}
972         return res
973
974     _columns = {
975         'slip_ids':fields.one2many('hr.payslip', 'employee_id', 'Payslips', required=False, readonly=True),
976         '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."),
977     }
978
979
980 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: