[MERGE] OPW 574895: fix incorrect matching of column headers when importing CSV files
[odoo/odoo.git] / addons / hr_payroll / hr_payroll.py
1 #-*- coding:utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
6 #    d$
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU Affero General Public License as published by
10 #    the Free Software Foundation, either version 3 of the License, or
11 #    (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU Affero General Public License for more details.
17 #
18 #    You should have received a copy of the GNU Affero General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23 import time
24 from datetime import date
25 from datetime import datetime
26 from datetime import timedelta
27 from dateutil import relativedelta
28
29 import netsvc
30 from osv import fields, osv
31 import tools
32 from tools.translate import _
33 import decimal_precision as dp
34
35 from tools.safe_eval import safe_eval as eval
36
37 class hr_payroll_structure(osv.osv):
38     """
39     Salary structure used to defined
40     - Basic
41     - Allowances
42     - Deductions
43     """
44
45     _name = 'hr.payroll.structure'
46     _description = 'Salary Structure'
47     _columns = {
48         'name':fields.char('Name', size=256, required=True),
49         'code':fields.char('Reference', size=64, required=True),
50         'company_id':fields.many2one('res.company', 'Company', required=True),
51         'note': fields.text('Description'),
52         'parent_id':fields.many2one('hr.payroll.structure', 'Parent'),
53         'children_ids':fields.one2many('hr.payroll.structure', 'parent_id', 'Children'),
54     }
55
56     def _get_parent(self, cr, uid, context=None):
57         obj_model = self.pool.get('ir.model.data')
58         res = False
59         data_id = obj_model.search(cr, uid, [('model', '=', 'hr.payroll.structure'), ('name', '=', 'structure_base')])
60         if data_id:
61             res = obj_model.browse(cr, uid, data_id[0], context=context).res_id
62         return res
63
64     _defaults = {
65         'company_id': lambda self, cr, uid, context: \
66                 self.pool.get('res.users').browse(cr, uid, uid,
67                     context=context).company_id.id,
68         'parent_id': _get_parent,
69     }
70
71     def copy(self, cr, uid, id, default=None, context=None):
72         """
73         Create a new record in hr_payroll_structure model from existing one
74         @param cr: cursor to database
75         @param user: id of current user
76         @param id: list of record ids on which copy method executes
77         @param default: dict type contains the values to be override during copy of object
78         @param context: context arguments, like lang, time zone
79
80         @return: returns a id of newly created record
81         """
82         if not default:
83             default = {}
84         default.update({
85             'code': self.browse(cr, uid, id, context=context).code + "(copy)",
86             'company_id': self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
87         })
88         return super(hr_payroll_structure, self).copy(cr, uid, id, default, context=context)
89
90     def get_all_rules(self, cr, uid, structure_ids, context=None):
91         """
92         @param structure_ids: list of structure
93         @return: returns a list of tuple (id, sequence) of rules that are maybe to apply
94         """
95
96         all_rules = []
97         for struct in self.browse(cr, uid, structure_ids, context=context):
98             all_rules += self.pool.get('hr.salary.rule')._recursive_search_of_rules(cr, uid, struct.rule_ids, context=context)
99         return all_rules
100
101     def _get_parent_structure(self, cr, uid, struct_ids, context=None):
102         if not struct_ids:
103             return []
104         parent = []
105         for struct in self.browse(cr, uid, struct_ids, context=context):
106             if struct.parent_id:
107                 parent.append(struct.parent_id.id)
108         if parent:
109             parent = self._get_parent_structure(cr, uid, parent, context)
110         return parent + struct_ids
111
112 hr_payroll_structure()
113
114 class hr_contract(osv.osv):
115     """
116     Employee contract based on the visa, work permits
117     allows to configure different Salary structure
118     """
119
120     _inherit = 'hr.contract'
121     _description = 'Employee Contract'
122     _columns = {
123         'struct_id': fields.many2one('hr.payroll.structure', 'Salary Structure'),
124         'schedule_pay': fields.selection([
125             ('monthly', 'Monthly'),
126             ('quarterly', 'Quarterly'),
127             ('semi-annually', 'Semi-annually'),
128             ('annually', 'Annually'),
129             ('weekly', 'Weekly'),
130             ('bi-weekly', 'Bi-weekly'),
131             ('bi-monthly', 'Bi-monthly'),
132             ], 'Scheduled Pay', select=True),
133     }
134
135     _defaults = {
136         'schedule_pay': 'monthly',
137     }
138
139     def get_all_structures(self, cr, uid, contract_ids, context=None):
140         """
141         @param contract_ids: list of contracts
142         @return: the structures linked to the given contracts, ordered by hierachy (parent=False first, then first level children and so on) and without duplicata
143         """
144         all_structures = []
145         structure_ids = [contract.struct_id.id for contract in self.browse(cr, uid, contract_ids, context=context)]
146         return list(set(self.pool.get('hr.payroll.structure')._get_parent_structure(cr, uid, structure_ids, context=context)))
147
148 hr_contract()
149
150 class contrib_register(osv.osv):
151     '''
152     Contribution Register
153     '''
154
155     _name = 'hr.contribution.register'
156     _description = 'Contribution Register'
157
158     _columns = {
159         'company_id':fields.many2one('res.company', 'Company', required=False),
160         'name':fields.char('Name', size=256, required=True, readonly=False),
161         'register_line_ids':fields.one2many('hr.payslip.line', 'register_id', 'Register Line', readonly=True),
162         'note': fields.text('Description'),
163     }
164     _defaults = {
165         'company_id': lambda self, cr, uid, context: \
166                 self.pool.get('res.users').browse(cr, uid, uid,
167                     context=context).company_id.id,
168     }
169
170 contrib_register()
171
172 class hr_salary_rule_category(osv.osv):
173     """
174     HR Salary Rule Category
175     """
176
177     _name = 'hr.salary.rule.category'
178     _description = 'Salary Rule Category'
179     _columns = {
180         'name':fields.char('Name', size=64, required=True, readonly=False),
181         'code':fields.char('Code', size=64, required=True, readonly=False),
182         'parent_id':fields.many2one('hr.salary.rule.category', 'Parent', help="Linking a salary category to its parent is used only for the reporting purpose."),
183         'children_ids': fields.one2many('hr.salary.rule.category', 'parent_id', 'Children'),
184         'note': fields.text('Description'),
185         'company_id':fields.many2one('res.company', 'Company', required=False),
186     }
187
188     _defaults = {
189         'company_id': lambda self, cr, uid, context: \
190                 self.pool.get('res.users').browse(cr, uid, uid,
191                     context=context).company_id.id,
192     }
193
194 hr_salary_rule_category()
195
196 class one2many_mod2(fields.one2many):
197
198     def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
199         if context is None:
200             context = {}
201         if not values:
202             values = {}
203         res = {}
204         for id in ids:
205             res[id] = []
206         ids2 = obj.pool.get(self._obj).search(cr, user, [(self._fields_id,'in',ids), ('appears_on_payslip', '=', True)], limit=self._limit)
207         for r in obj.pool.get(self._obj)._read_flat(cr, user, ids2, [self._fields_id], context=context, load='_classic_write'):
208             res[r[self._fields_id]].append( r['id'] )
209         return res
210
211 class hr_payslip_run(osv.osv):
212
213     _name = 'hr.payslip.run'
214     _description = 'Payslip 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         ], 'State', select=True, readonly=True),
222         'date_start': fields.date('Date From', required=True, readonly=True, states={'draft': [('readonly', False)]}),
223         'date_end': fields.date('Date To', required=True, readonly=True, states={'draft': [('readonly', False)]}),
224         'credit_note': fields.boolean('Credit Note', readonly=True, states={'draft': [('readonly', False)]}, help="If its checked, indicates that all payslips generated from here are refund payslips."),
225     }
226     _defaults = {
227         'state': 'draft',
228         'date_start': lambda *a: time.strftime('%Y-%m-01'),
229         'date_end': lambda *a: str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10],
230     }
231
232     def draft_payslip_run(self, cr, uid, ids, context=None):
233         return self.write(cr, uid, ids, {'state': 'draft'}, context=context)
234
235     def close_payslip_run(self, cr, uid, ids, context=None):
236         return self.write(cr, uid, ids, {'state': 'close'}, context=context)
237
238 hr_payslip_run()
239
240 class hr_payslip(osv.osv):
241     '''
242     Pay Slip
243     '''
244
245     _name = 'hr.payslip'
246     _description = 'Pay Slip'
247
248     def _get_lines_salary_rule_category(self, cr, uid, ids, field_names, arg=None, context=None):
249         result = {}
250         if not ids: return result
251         for id in ids:
252             result.setdefault(id, [])
253         cr.execute('''SELECT pl.slip_id, pl.id FROM hr_payslip_line AS pl \
254                     LEFT JOIN hr_salary_rule_category AS sh on (pl.category_id = sh.id) \
255                     WHERE pl.slip_id in %s \
256                     GROUP BY pl.slip_id, pl.sequence, pl.id ORDER BY pl.sequence''',(tuple(ids),))
257         res = cr.fetchall()
258         for r in res:
259             result[r[0]].append(r[1])
260         return result
261
262     _columns = {
263         'struct_id': fields.many2one('hr.payroll.structure', 'Structure', readonly=True, states={'draft': [('readonly', False)]}, help='Defines the rules that have to be applied to this payslip, accordingly to the contract chosen. If you let empty the field contract, this field isn\'t mandatory anymore and thus the rules applied will be all the rules set on the structure of all contracts of the employee valid for the chosen period'),
264         'name': fields.char('Description', size=64, required=False, readonly=True, states={'draft': [('readonly', False)]}),
265         'number': fields.char('Reference', size=64, required=False, readonly=True, states={'draft': [('readonly', False)]}),
266         'employee_id': fields.many2one('hr.employee', 'Employee', required=True, readonly=True, states={'draft': [('readonly', False)]}),
267         'date_from': fields.date('Date From', readonly=True, states={'draft': [('readonly', False)]}, required=True),
268         'date_to': fields.date('Date To', readonly=True, states={'draft': [('readonly', False)]}, required=True),
269         'state': fields.selection([
270             ('draft', 'Draft'),
271             ('verify', 'Waiting'),
272             ('done', 'Done'),
273             ('cancel', 'Rejected'),
274         ], 'State', select=True, readonly=True,
275             help='* When the payslip is created the state is \'Draft\'.\
276             \n* If the payslip is under verification, the state is \'Waiting\'. \
277             \n* If the payslip is confirmed then state is set to \'Done\'.\
278             \n* When user cancel payslip the state is \'Rejected\'.'),
279 #        'line_ids': fields.one2many('hr.payslip.line', 'slip_id', 'Payslip Line', required=False, readonly=True, states={'draft': [('readonly', False)]}),
280         'line_ids': one2many_mod2('hr.payslip.line', 'slip_id', 'Payslip Lines', readonly=True, states={'draft':[('readonly',False)]}),
281         'company_id': fields.many2one('res.company', 'Company', required=False, readonly=True, states={'draft': [('readonly', False)]}),
282         'worked_days_line_ids': fields.one2many('hr.payslip.worked_days', 'payslip_id', 'Payslip Worked Days', required=False, readonly=True, states={'draft': [('readonly', False)]}),
283         'input_line_ids': fields.one2many('hr.payslip.input', 'payslip_id', 'Payslip Inputs', required=False, readonly=True, states={'draft': [('readonly', False)]}),
284         'paid': fields.boolean('Made Payment Order ? ', required=False, readonly=True, states={'draft': [('readonly', False)]}),
285         'note': fields.text('Description', readonly=True, states={'draft':[('readonly',False)]}),
286         'contract_id': fields.many2one('hr.contract', 'Contract', required=False, readonly=True, states={'draft': [('readonly', False)]}),
287         'details_by_salary_rule_category': fields.function(_get_lines_salary_rule_category, method=True, type='one2many', relation='hr.payslip.line', string='Details by Salary Rule Category'),
288         'credit_note': fields.boolean('Credit Note', help="Indicates this payslip has a refund of another", readonly=True, states={'draft': [('readonly', False)]}),
289         'payslip_run_id': fields.many2one('hr.payslip.run', 'Payslip 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             'move_ids': [],
316             'move_line_ids': [],
317             'company_id': company_id,
318             'period_id': False,
319             'basic_before_leaves': 0.0,
320             'basic_amount': 0.0
321         })
322         return super(hr_payslip, self).copy(cr, uid, id, default, context=context)
323
324     def cancel_sheet(self, cr, uid, ids, context=None):
325         return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
326
327     def process_sheet(self, cr, uid, ids, context=None):
328         return self.write(cr, uid, ids, {'paid': True, 'state': 'done'}, context=context)
329
330     def hr_verify_sheet(self, cr, uid, ids, context=None):
331         return self.write(cr, uid, ids, {'state': 'verify'}, context=context)
332
333     def refund_sheet(self, cr, uid, ids, context=None):
334         mod_obj = self.pool.get('ir.model.data')
335         wf_service = netsvc.LocalService("workflow")
336         for payslip in self.browse(cr, uid, ids, context=context):
337             id_copy = self.copy(cr, uid, payslip.id, {'credit_note': True, 'name': _('Refund: ')+payslip.name}, context=context)
338             self.compute_sheet(cr, uid, [id_copy], context=context)
339             wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'hr_verify_sheet', cr)
340             wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'process_sheet', cr)
341
342         form_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_form')
343         form_res = form_id and form_id[1] or False
344         tree_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_tree')
345         tree_res = tree_id and tree_id[1] or False
346         return {
347             'name':_("Refund Payslip"),
348             'view_mode': 'tree, form',
349             'view_id': False,
350             'view_type': 'form',
351             'res_model': 'hr.payslip',
352             'type': 'ir.actions.act_window',
353             'nodestroy': True,
354             'target': 'current',
355             'domain': "[('id', 'in', %s)]" % [id_copy],
356             'views': [(tree_res, 'tree'), (form_res, 'form')],
357             'context': {}
358         }
359
360     def check_done(self, cr, uid, ids, context=None):
361         return True
362
363     #TODO move this function into hr_contract module, on hr.employee object
364     def get_contract(self, cr, uid, employee, date_from, date_to, context=None):
365         """
366         @param employee: browse record of employee
367         @param date_from: date field
368         @param date_to: date field
369         @return: returns the ids of all the contracts for the given employee that need to be considered for the given dates
370         """
371         contract_obj = self.pool.get('hr.contract')
372         clause = []
373         #a contract is valid if it ends between the given dates
374         clause_1 = ['&',('date_end', '<=', date_to),('date_end','>=', date_from)]
375         #OR if it starts between the given dates
376         clause_2 = ['&',('date_start', '<=', date_to),('date_start','>=', date_from)]
377         #OR if it starts before the date_from and finish after the date_end (or never finish)
378         clause_3 = [('date_start','<=', date_from),'|',('date_end', '=', False),('date_end','>=', date_to)]
379         clause_final =  [('employee_id', '=', employee.id),'|','|'] + clause_1 + clause_2 + clause_3
380         contract_ids = contract_obj.search(cr, uid, clause_final, context=context)
381         return contract_ids
382
383     def compute_sheet(self, cr, uid, ids, context=None):
384         slip_line_pool = self.pool.get('hr.payslip.line')
385         sequence_obj = self.pool.get('ir.sequence')
386         for payslip in self.browse(cr, uid, ids, context=context):
387             number = payslip.number or sequence_obj.get(cr, uid, 'salary.slip')
388             #delete old payslip lines
389             old_slipline_ids = slip_line_pool.search(cr, uid, [('slip_id', '=', payslip.id)], context=context)
390 #            old_slipline_ids
391             if old_slipline_ids:
392                 slip_line_pool.unlink(cr, uid, old_slipline_ids, context=context)
393             if payslip.contract_id:
394                 #set the list of contract for which the rules have to be applied
395                 contract_ids = [payslip.contract_id.id]
396             else:
397                 #if we don't give the contract, then the rules to apply should be for all current contracts of the employee
398                 contract_ids = self.get_contract(cr, uid, payslip.employee_id, payslip.date_from, payslip.date_to, context=context)
399             lines = [(0,0,line) for line in self.pool.get('hr.payslip').get_payslip_lines(cr, uid, contract_ids, payslip.id, context=context)]
400             self.write(cr, uid, [payslip.id], {'line_ids': lines, 'number': number,}, context=context)
401         return True
402
403     def get_worked_day_lines(self, cr, uid, contract_ids, date_from, date_to, context=None):
404         """
405         @param contract_ids: list of contract id
406         @return: returns a list of dict containing the input that should be applied for the given contract between date_from and date_to
407         """
408         def was_on_leave(employee_id, datetime_day, context=None):
409             res = False
410             day = datetime_day.strftime("%Y-%m-%d")
411             holiday_ids = self.pool.get('hr.holidays').search(cr, uid, [('state','=','validate'),('employee_id','=',employee_id),('type','=','remove'),('date_from','<=',day),('date_to','>=',day)])
412             if holiday_ids:
413                 res = self.pool.get('hr.holidays').browse(cr, uid, holiday_ids, context=context)[0].holiday_status_id.name
414             return res
415
416         res = []
417         for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
418             if not contract.working_hours:
419                 #fill only if the contract as a working schedule linked
420                 continue
421             attendances = {
422                  'name': _("Normal Working Days paid at 100%"),
423                  'sequence': 1,
424                  'code': 'WORK100',
425                  'number_of_days': 0.0,
426                  'number_of_hours': 0.0,
427                  'contract_id': contract.id,
428             }
429             leaves = {}
430             day_from = datetime.strptime(date_from,"%Y-%m-%d")
431             day_to = datetime.strptime(date_to,"%Y-%m-%d")
432             nb_of_days = (day_to - day_from).days + 1
433             for day in range(0, nb_of_days):
434                 working_hours_on_day = self.pool.get('resource.calendar').working_hours_on_day(cr, uid, contract.working_hours, day_from + timedelta(days=day), context)
435                 if working_hours_on_day:
436                     #the employee had to work
437                     leave_type = was_on_leave(contract.employee_id.id, day_from + timedelta(days=day), context=context)
438                     if leave_type:
439                         #if he was on leave, fill the leaves dict
440                         if leave_type in leaves:
441                             leaves[leave_type]['number_of_days'] += 1.0
442                             leaves[leave_type]['number_of_hours'] += working_hours_on_day
443                         else:
444                             leaves[leave_type] = {
445                                 'name': leave_type,
446                                 'sequence': 5,
447                                 'code': leave_type,
448                                 'number_of_days': 1.0,
449                                 'number_of_hours': working_hours_on_day,
450                                 'contract_id': contract.id,
451                             }
452                     else:
453                         #add the input vals to tmp (increment if existing)
454                         attendances['number_of_days'] += 1.0
455                         attendances['number_of_hours'] += working_hours_on_day
456             leaves = [value for key,value in leaves.items()]
457             res += [attendances] + leaves
458         return res
459
460     def get_inputs(self, cr, uid, contract_ids, date_from, date_to, context=None):
461         res = []
462         contract_obj = self.pool.get('hr.contract')
463         rule_obj = self.pool.get('hr.salary.rule')
464
465         structure_ids = contract_obj.get_all_structures(cr, uid, contract_ids, context=context)
466         rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
467         sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
468
469         for contract in contract_obj.browse(cr, uid, contract_ids, context=context):
470             for rule in rule_obj.browse(cr, uid, sorted_rule_ids, context=context):
471                 if rule.input_ids:
472                     for input in rule.input_ids:
473                         inputs = {
474                              'name': input.name,
475                              'code': input.code,
476                              'contract_id': contract.id,
477                         }
478                         res += [inputs]
479         return res
480
481     def get_payslip_lines(self, cr, uid, contract_ids, payslip_id, context):
482         def _sum_salary_rule_category(localdict, category, amount):
483             if category.parent_id:
484                 localdict = _sum_salary_rule_category(localdict, category.parent_id, amount)
485             localdict['categories'].dict[category.code] = category.code in localdict['categories'].dict and localdict['categories'].dict[category.code] + amount or amount
486             return localdict
487
488         class BrowsableObject(object):
489             def __init__(self, pool, cr, uid, employee_id, dict):
490                 self.pool = pool
491                 self.cr = cr
492                 self.uid = uid
493                 self.employee_id = employee_id
494                 self.dict = dict
495
496             def __getattr__(self, attr):
497                 return attr in self.dict and self.dict.__getitem__(attr) or 0.0
498
499         class InputLine(BrowsableObject):
500             """a class that will be used into the python code, mainly for usability purposes"""
501             def sum(self, code, from_date, to_date=None):
502                 if to_date is None:
503                     to_date = datetime.now().strftime('%Y-%m-%d')
504                 result = 0.0
505                 self.cr.execute("SELECT sum(amount) as sum\
506                             FROM hr_payslip as hp, hr_payslip_input as pi \
507                             WHERE hp.employee_id = %s AND hp.state = 'done' \
508                             AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s",
509                            (self.employee_id, from_date, to_date, code))
510                 res = self.cr.fetchone()[0]
511                 return res or 0.0
512
513         class WorkedDays(BrowsableObject):
514             """a class that will be used into the python code, mainly for usability purposes"""
515             def _sum(self, code, from_date, to_date=None):
516                 if to_date is None:
517                     to_date = datetime.now().strftime('%Y-%m-%d')
518                 result = 0.0
519                 self.cr.execute("SELECT sum(number_of_days) as number_of_days, sum(number_of_hours) as number_of_hours\
520                             FROM hr_payslip as hp, hr_payslip_worked_days as pi \
521                             WHERE hp.employee_id = %s AND hp.state = 'done'\
522                             AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s",
523                            (self.employee_id, from_date, to_date, code))
524                 return self.cr.fetchone()
525
526             def sum(self, code, from_date, to_date=None):
527                 res = self._sum(code, from_date, to_date)
528                 return res and res[0] or 0.0
529
530             def sum_hours(self, code, from_date, to_date=None):
531                 res = self._sum(code, from_date, to_date)
532                 return res and res[1] or 0.0
533
534         class Payslips(BrowsableObject):
535             """a class that will be used into the python code, mainly for usability purposes"""
536
537             def sum(self, code, from_date, to_date=None):
538                 if to_date is None:
539                     to_date = datetime.now().strftime('%Y-%m-%d')
540                 self.cr.execute("SELECT sum(case when hp.credit_note = False then (pl.total) else (-pl.total) end)\
541                             FROM hr_payslip as hp, hr_payslip_line as pl \
542                             WHERE hp.employee_id = %s AND hp.state = 'done' \
543                             AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id AND pl.code = %s",
544                             (self.employee_id, from_date, to_date, code))
545                 res = self.cr.fetchone()
546                 return res and res[0] or 0.0
547
548         #we keep a dict with the result because a value can be overwritten by another rule with the same code
549         result_dict = {}
550         rules = {}
551         categories_dict = {}
552         blacklist = []
553         payslip_obj = self.pool.get('hr.payslip')
554         inputs_obj = self.pool.get('hr.payslip.worked_days')
555         obj_rule = self.pool.get('hr.salary.rule')
556         payslip = payslip_obj.browse(cr, uid, payslip_id, context=context)
557         worked_days = {}
558         for worked_days_line in payslip.worked_days_line_ids:
559             worked_days[worked_days_line.code] = worked_days_line
560         inputs = {}
561         for input_line in payslip.input_line_ids:
562             inputs[input_line.code] = input_line
563
564         categories_obj = BrowsableObject(self.pool, cr, uid, payslip.employee_id.id, categories_dict)
565         input_obj = InputLine(self.pool, cr, uid, payslip.employee_id.id, inputs)
566         worked_days_obj = WorkedDays(self.pool, cr, uid, payslip.employee_id.id, worked_days)
567         payslip_obj = Payslips(self.pool, cr, uid, payslip.employee_id.id, payslip)
568         rules_obj = BrowsableObject(self.pool, cr, uid, payslip.employee_id.id, rules)
569
570         localdict = {'categories': categories_obj, 'rules': rules_obj, 'payslip': payslip_obj, 'worked_days': worked_days_obj, 'inputs': input_obj}
571         #get the ids of the structures on the contracts and their parent id as well
572         structure_ids = self.pool.get('hr.contract').get_all_structures(cr, uid, contract_ids, context=context)
573         #get the rules of the structure and thier children
574         rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
575         #run the rules by sequence
576         sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
577
578         for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
579             employee = contract.employee_id
580             localdict.update({'employee': employee, 'contract': contract})
581             for rule in obj_rule.browse(cr, uid, sorted_rule_ids, context=context):
582                 key = rule.code + '-' + str(contract.id)
583                 localdict['result'] = None
584                 localdict['result_qty'] = 1.0
585                 #check if the rule can be applied
586                 if obj_rule.satisfy_condition(cr, uid, rule.id, localdict, context=context) and rule.id not in blacklist:
587                     #compute the amount of the rule
588                     amount, qty, rate = obj_rule.compute_rule(cr, uid, rule.id, localdict, context=context)
589                     #check if there is already a rule computed with that code
590                     previous_amount = rule.code in localdict and localdict[rule.code] or 0.0
591                     #set/overwrite the amount computed for this rule in the localdict
592                     tot_rule = amount * qty * rate / 100.0
593                     localdict[rule.code] = tot_rule
594                     rules[rule.code] = rule
595                     #sum the amount for its salary category
596                     localdict = _sum_salary_rule_category(localdict, rule.category_id, tot_rule - previous_amount)
597                     #create/overwrite the rule in the temporary results
598                     result_dict[key] = {
599                         'salary_rule_id': rule.id,
600                         'contract_id': contract.id,
601                         'name': rule.name,
602                         'code': rule.code,
603                         'category_id': rule.category_id.id,
604                         'sequence': rule.sequence,
605                         'appears_on_payslip': rule.appears_on_payslip,
606                         'condition_select': rule.condition_select,
607                         'condition_python': rule.condition_python,
608                         'condition_range': rule.condition_range,
609                         'condition_range_min': rule.condition_range_min,
610                         'condition_range_max': rule.condition_range_max,
611                         'amount_select': rule.amount_select,
612                         'amount_fix': rule.amount_fix,
613                         'amount_python_compute': rule.amount_python_compute,
614                         'amount_percentage': rule.amount_percentage,
615                         'amount_percentage_base': rule.amount_percentage_base,
616                         'register_id': rule.register_id.id,
617                         'amount': amount,
618                         'employee_id': contract.employee_id.id,
619                         'quantity': qty,
620                         'rate': rate,
621                     }
622                 else:
623                     #blacklist this rule and its children
624                     blacklist += [id for id, seq in self.pool.get('hr.salary.rule')._recursive_search_of_rules(cr, uid, [rule], context=context)]
625
626         result = [value for code, value in result_dict.items()]
627         return result
628
629     def onchange_employee_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
630         empolyee_obj = self.pool.get('hr.employee')
631         contract_obj = self.pool.get('hr.contract')
632         worked_days_obj = self.pool.get('hr.payslip.worked_days')
633         input_obj = self.pool.get('hr.payslip.input')
634
635         if context is None:
636             context = {}
637         #delete old worked days lines
638         old_worked_days_ids = ids and worked_days_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
639         if old_worked_days_ids:
640             worked_days_obj.unlink(cr, uid, old_worked_days_ids, context=context)
641
642         #delete old input lines
643         old_input_ids = ids and input_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
644         if old_input_ids:
645             input_obj.unlink(cr, uid, old_input_ids, context=context)
646
647
648         #defaults
649         res = {'value':{
650                       'line_ids':[],
651                       'input_line_ids': [],
652                       'worked_days_line_ids': [],
653                       #'details_by_salary_head':[], TODO put me back
654                       'name':'',
655                       'contract_id': False,
656                       'struct_id': False,
657                       }
658             }
659         if (not employee_id) or (not date_from) or (not date_to):
660             return res
661         ttyme = datetime.fromtimestamp(time.mktime(time.strptime(date_from, "%Y-%m-%d")))
662         employee_id = empolyee_obj.browse(cr, uid, employee_id, context=context)
663         res['value'].update({
664                     'name': _('Salary Slip of %s for %s') % (employee_id.name, tools.ustr(ttyme.strftime('%B-%Y'))),
665                     'company_id': employee_id.company_id.id
666         })
667
668         if not context.get('contract', False):
669             #fill with the first contract of the employee
670             contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
671             res['value'].update({
672                         'struct_id': contract_ids and contract_obj.read(cr, uid, contract_ids[0], ['struct_id'], context=context)['struct_id'][0] or False,
673                         'contract_id': contract_ids and contract_ids[0] or False,
674             })
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                 #fill the structure with the one on the selected contract
680                 contract_record = contract_obj.browse(cr, uid, contract_id, context=context)
681                 res['value'].update({
682                             'struct_id': contract_record.struct_id.id,
683                             'contract_id': contract_id
684                 })
685             else:
686                 #if we don't give the contract, then the input to fill should be for all current contracts of the employee
687                 contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
688                 if not contract_ids:
689                     return res
690
691         #computation of the salary input
692         worked_days_line_ids = self.get_worked_day_lines(cr, uid, contract_ids, date_from, date_to, context=context)
693         input_line_ids = self.get_inputs(cr, uid, contract_ids, date_from, date_to, context=context)
694         res['value'].update({
695                     'worked_days_line_ids': worked_days_line_ids,
696                     'input_line_ids': input_line_ids,
697         })
698         return res
699
700     def onchange_contract_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
701 #TODO it seems to be the mess in the onchanges, we should have onchange_employee => onchange_contract => doing all the things
702         if context is None:
703             context = {}
704         res = {'value':{
705                  'line_ids': [],
706                  'name': '',
707                  }
708               }
709         context.update({'contract': True})
710         if not contract_id:
711             res['value'].update({'struct_id': False})
712         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)
713
714 hr_payslip()
715
716 class hr_payslip_worked_days(osv.osv):
717     '''
718     Payslip Worked Days
719     '''
720
721     _name = 'hr.payslip.worked_days'
722     _description = 'Payslip Worked Days'
723     _columns = {
724         'name': fields.char('Description', size=256, required=True),
725         'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade', select=True),
726         'sequence': fields.integer('Sequence', required=True, select=True),
727         'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
728         'number_of_days': fields.float('Number of Days'),
729         'number_of_hours': fields.float('Number of Hours'),
730         'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
731     }
732     _order = 'payslip_id, sequence'
733     _defaults = {
734         'sequence': 10,
735     }
736 hr_payslip_worked_days()
737
738 class hr_payslip_input(osv.osv):
739     '''
740     Payslip Input
741     '''
742
743     _name = 'hr.payslip.input'
744     _description = 'Payslip Input'
745     _columns = {
746         'name': fields.char('Description', size=256, required=True),
747         'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade', select=True),
748         'sequence': fields.integer('Sequence', required=True, select=True),
749         'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
750         '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."),
751         'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
752     }
753     _order = 'payslip_id, sequence'
754     _defaults = {
755         'sequence': 10,
756         'amount': 0.0,
757     }
758
759 hr_payslip_input()
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 for the display of 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.'),#old name = conditions
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 hr_salary_rule()
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 hr_rule_input()
913
914 class hr_payslip_line(osv.osv):
915     '''
916     Payslip Line
917     '''
918
919     _name = 'hr.payslip.line'
920     _inherit = 'hr.salary.rule'
921     _description = 'Payslip Line'
922     _order = 'contract_id, sequence'
923
924     def _calculate_total(self, cr, uid, ids, name, args, context):
925         if not ids: return {}
926         res = {}
927         for line in self.browse(cr, uid, ids, context=context):
928             res[line.id] = float(line.quantity) * line.amount * line.rate / 100
929         return res
930
931     _columns = {
932         'slip_id':fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade'),
933         'salary_rule_id':fields.many2one('hr.salary.rule', 'Rule', required=True),
934         'employee_id':fields.many2one('hr.employee', 'Employee', required=True),
935         'contract_id':fields.many2one('hr.contract', 'Contract', required=True, select=True),
936         'rate': fields.float('Rate (%)', digits_compute=dp.get_precision('Payroll Rate')),
937         'amount': fields.float('Amount', digits_compute=dp.get_precision('Payroll')),
938         'quantity': fields.float('Quantity', digits_compute=dp.get_precision('Payroll')),
939         'total': fields.function(_calculate_total, method=True, type='float', string='Total', digits_compute=dp.get_precision('Payroll'),store=True ),
940     }
941
942     _defaults = {
943         'quantity': 1.0,
944         'rate': 100.0,
945     }
946
947 hr_payslip_line()
948
949 class hr_payroll_structure(osv.osv):
950
951     _inherit = 'hr.payroll.structure'
952     _columns = {
953         'rule_ids':fields.many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id', 'Salary Rules'),
954     }
955
956 hr_payroll_structure()
957
958 class hr_employee(osv.osv):
959     '''
960     Employee
961     '''
962
963     _inherit = 'hr.employee'
964     _description = 'Employee'
965
966     def _calculate_total_wage(self, cr, uid, ids, name, args, context):
967         if not ids: return {}
968         res = {}
969         current_date = datetime.now().strftime('%Y-%m-%d')
970         for employee in self.browse(cr, uid, ids, context=context):
971             if not employee.contract_ids:
972                 res[employee.id] = {'basic': 0.0}
973                 continue
974             cr.execute( 'SELECT SUM(wage) '\
975                         'FROM hr_contract '\
976                         'WHERE employee_id = %s '\
977                         'AND date_start <= %s '\
978                         'AND (date_end > %s OR date_end is NULL)',
979                          (employee.id, current_date, current_date))
980             result = dict(cr.dictfetchone())
981             res[employee.id] = {'basic': result['sum']}
982         return res
983
984     _columns = {
985         'slip_ids':fields.one2many('hr.payslip', 'employee_id', 'Payslips', required=False, readonly=True),
986         '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."),
987     }
988
989 hr_employee()
990
991 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: