[FIX] account: incorrect usage of browse_record in fix for accounting reports merged...
[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=_("%s (copy)") % (self.browse(cr, uid, id, context=context).code),
86             company_id=self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id)
87         return super(hr_payroll_structure, self).copy(cr, uid, id, default, context=context)
88
89     def get_all_rules(self, cr, uid, structure_ids, context=None):
90         """
91         @param structure_ids: list of structure
92         @return: returns a list of tuple (id, sequence) of rules that are maybe to apply
93         """
94
95         all_rules = []
96         for struct in self.browse(cr, uid, structure_ids, context=context):
97             all_rules += self.pool.get('hr.salary.rule')._recursive_search_of_rules(cr, uid, struct.rule_ids, context=context)
98         return all_rules
99
100     def _get_parent_structure(self, cr, uid, struct_ids, context=None):
101         if not struct_ids:
102             return []
103         parent = []
104         for struct in self.browse(cr, uid, struct_ids, context=context):
105             if struct.parent_id:
106                 parent.append(struct.parent_id.id)
107         if parent:
108             parent = self._get_parent_structure(cr, uid, parent, context)
109         return parent + struct_ids
110
111 hr_payroll_structure()
112
113 class hr_contract(osv.osv):
114     """
115     Employee contract based on the visa, work permits
116     allows to configure different Salary structure
117     """
118
119     _inherit = 'hr.contract'
120     _description = 'Employee Contract'
121     _columns = {
122         'struct_id': fields.many2one('hr.payroll.structure', 'Salary Structure'),
123         'schedule_pay': fields.selection([
124             ('monthly', 'Monthly'),
125             ('quarterly', 'Quarterly'),
126             ('semi-annually', 'Semi-annually'),
127             ('annually', 'Annually'),
128             ('weekly', 'Weekly'),
129             ('bi-weekly', 'Bi-weekly'),
130             ('bi-monthly', 'Bi-monthly'),
131             ], 'Scheduled Pay', select=True),
132     }
133
134     _defaults = {
135         'schedule_pay': 'monthly',
136     }
137
138     def get_all_structures(self, cr, uid, contract_ids, context=None):
139         """
140         @param contract_ids: list of contracts
141         @return: the structures linked to the given contracts, ordered by hierachy (parent=False first, then first level children and so on) and without duplicata
142         """
143         all_structures = []
144         structure_ids = [contract.struct_id.id for contract in self.browse(cr, uid, contract_ids, context=context)]
145         return list(set(self.pool.get('hr.payroll.structure')._get_parent_structure(cr, uid, structure_ids, context=context)))
146
147 hr_contract()
148
149 class contrib_register(osv.osv):
150     '''
151     Contribution Register
152     '''
153
154     _name = 'hr.contribution.register'
155     _description = 'Contribution Register'
156
157     _columns = {
158         'company_id':fields.many2one('res.company', 'Company'),
159         'partner_id':fields.many2one('res.partner', 'Partner'),
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         ], 'Status', select=True, readonly=True),
222         'date_start': fields.date('Date From', required=True, readonly=True, states={'draft': [('readonly', False)]}),
223         'date_end': fields.date('Date To', required=True, readonly=True, states={'draft': [('readonly', False)]}),
224         'credit_note': fields.boolean('Credit Note', readonly=True, states={'draft': [('readonly', False)]}, help="If its checked, indicates that all payslips generated from here are refund payslips."),
225     }
226     _defaults = {
227         'state': 'draft',
228         'date_start': lambda *a: time.strftime('%Y-%m-01'),
229         'date_end': lambda *a: str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10],
230     }
231
232     def draft_payslip_run(self, cr, uid, ids, context=None):
233         return self.write(cr, uid, ids, {'state': 'draft'}, context=context)
234
235     def close_payslip_run(self, cr, uid, ids, context=None):
236         return self.write(cr, uid, ids, {'state': 'close'}, context=context)
237
238 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         ], '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('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             '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         wf_service = netsvc.LocalService("workflow")
335         for payslip in self.browse(cr, uid, ids, context=context):
336             id_copy = self.copy(cr, uid, payslip.id, {'credit_note': True, 'name': _('Refund: ')+payslip.name}, context=context)
337             self.compute_sheet(cr, uid, [id_copy], context=context)
338             wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'hr_verify_sheet', cr)
339             wf_service.trg_validate(uid, 'hr.payslip', id_copy, 'process_sheet', cr)
340
341         form_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_form')
342         form_res = form_id and form_id[1] or False
343         tree_id = mod_obj.get_object_reference(cr, uid, 'hr_payroll', 'view_hr_payslip_tree')
344         tree_res = tree_id and tree_id[1] or False
345         return {
346             'name':_("Refund Payslip"),
347             'view_mode': 'tree, form',
348             'view_id': False,
349             'view_type': 'form',
350             'res_model': 'hr.payslip',
351             'type': 'ir.actions.act_window',
352             'nodestroy': True,
353             'target': 'current',
354             'domain': "[('id', 'in', %s)]" % [id_copy],
355             'views': [(tree_res, 'tree'), (form_res, 'form')],
356             'context': {}
357         }
358
359     def check_done(self, cr, uid, ids, context=None):
360         return True
361
362     #TODO move this function into hr_contract module, on hr.employee object
363     def get_contract(self, cr, uid, employee, date_from, date_to, context=None):
364         """
365         @param employee: browse record of employee
366         @param date_from: date field
367         @param date_to: date field
368         @return: returns the ids of all the contracts for the given employee that need to be considered for the given dates
369         """
370         contract_obj = self.pool.get('hr.contract')
371         clause = []
372         #a contract is valid if it ends between the given dates
373         clause_1 = ['&',('date_end', '<=', date_to),('date_end','>=', date_from)]
374         #OR if it starts between the given dates
375         clause_2 = ['&',('date_start', '<=', date_to),('date_start','>=', date_from)]
376         #OR if it starts before the date_from and finish after the date_end (or never finish)
377         clause_3 = [('date_start','<=', date_from),'|',('date_end', '=', False),('date_end','>=', date_to)]
378         clause_final =  [('employee_id', '=', employee.id),'|','|'] + clause_1 + clause_2 + clause_3
379         contract_ids = contract_obj.search(cr, uid, clause_final, context=context)
380         return contract_ids
381
382     def compute_sheet(self, cr, uid, ids, context=None):
383         slip_line_pool = self.pool.get('hr.payslip.line')
384         sequence_obj = self.pool.get('ir.sequence')
385         for payslip in self.browse(cr, uid, ids, context=context):
386             number = payslip.number or sequence_obj.get(cr, uid, 'salary.slip')
387             #delete old payslip lines
388             old_slipline_ids = slip_line_pool.search(cr, uid, [('slip_id', '=', payslip.id)], context=context)
389 #            old_slipline_ids
390             if old_slipline_ids:
391                 slip_line_pool.unlink(cr, uid, old_slipline_ids, context=context)
392             if payslip.contract_id:
393                 #set the list of contract for which the rules have to be applied
394                 contract_ids = [payslip.contract_id.id]
395             else:
396                 #if we don't give the contract, then the rules to apply should be for all current contracts of the employee
397                 contract_ids = self.get_contract(cr, uid, payslip.employee_id, payslip.date_from, payslip.date_to, context=context)
398             lines = [(0,0,line) for line in self.pool.get('hr.payslip').get_payslip_lines(cr, uid, contract_ids, payslip.id, context=context)]
399             self.write(cr, uid, [payslip.id], {'line_ids': lines, 'number': number,}, context=context)
400         return True
401
402     def get_worked_day_lines(self, cr, uid, contract_ids, date_from, date_to, context=None):
403         """
404         @param contract_ids: list of contract id
405         @return: returns a list of dict containing the input that should be applied for the given contract between date_from and date_to
406         """
407         def was_on_leave(employee_id, datetime_day, context=None):
408             res = False
409             day = datetime_day.strftime("%Y-%m-%d")
410             holiday_ids = self.pool.get('hr.holidays').search(cr, uid, [('state','=','validate'),('employee_id','=',employee_id),('type','=','remove'),('date_from','<=',day),('date_to','>=',day)])
411             if holiday_ids:
412                 res = self.pool.get('hr.holidays').browse(cr, uid, holiday_ids, context=context)[0].holiday_status_id.name
413             return res
414
415         res = []
416         for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
417             if not contract.working_hours:
418                 #fill only if the contract as a working schedule linked
419                 continue
420             attendances = {
421                  'name': _("Normal Working Days paid at 100%"),
422                  'sequence': 1,
423                  'code': 'WORK100',
424                  'number_of_days': 0.0,
425                  'number_of_hours': 0.0,
426                  'contract_id': contract.id,
427             }
428             leaves = {}
429             day_from = datetime.strptime(date_from,"%Y-%m-%d")
430             day_to = datetime.strptime(date_to,"%Y-%m-%d")
431             nb_of_days = (day_to - day_from).days + 1
432             for day in range(0, nb_of_days):
433                 working_hours_on_day = self.pool.get('resource.calendar').working_hours_on_day(cr, uid, contract.working_hours, day_from + timedelta(days=day), context)
434                 if working_hours_on_day:
435                     #the employee had to work
436                     leave_type = was_on_leave(contract.employee_id.id, day_from + timedelta(days=day), context=context)
437                     if leave_type:
438                         #if he was on leave, fill the leaves dict
439                         if leave_type in leaves:
440                             leaves[leave_type]['number_of_days'] += 1.0
441                             leaves[leave_type]['number_of_hours'] += working_hours_on_day
442                         else:
443                             leaves[leave_type] = {
444                                 'name': leave_type,
445                                 'sequence': 5,
446                                 'code': leave_type,
447                                 'number_of_days': 1.0,
448                                 'number_of_hours': working_hours_on_day,
449                                 'contract_id': contract.id,
450                             }
451                     else:
452                         #add the input vals to tmp (increment if existing)
453                         attendances['number_of_days'] += 1.0
454                         attendances['number_of_hours'] += working_hours_on_day
455             leaves = [value for key,value in leaves.items()]
456             res += [attendances] + leaves
457         return res
458
459     def get_inputs(self, cr, uid, contract_ids, date_from, date_to, context=None):
460         res = []
461         contract_obj = self.pool.get('hr.contract')
462         rule_obj = self.pool.get('hr.salary.rule')
463
464         structure_ids = contract_obj.get_all_structures(cr, uid, contract_ids, context=context)
465         rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
466         sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
467
468         for contract in contract_obj.browse(cr, uid, contract_ids, context=context):
469             for rule in rule_obj.browse(cr, uid, sorted_rule_ids, context=context):
470                 if rule.input_ids:
471                     for input in rule.input_ids:
472                         inputs = {
473                              'name': input.name,
474                              'code': input.code,
475                              'contract_id': contract.id,
476                         }
477                         res += [inputs]
478         return res
479
480     def get_payslip_lines(self, cr, uid, contract_ids, payslip_id, context):
481         def _sum_salary_rule_category(localdict, category, amount):
482             if category.parent_id:
483                 localdict = _sum_salary_rule_category(localdict, category.parent_id, amount)
484             localdict['categories'].dict[category.code] = category.code in localdict['categories'].dict and localdict['categories'].dict[category.code] + amount or amount
485             return localdict
486
487         class BrowsableObject(object):
488             def __init__(self, pool, cr, uid, employee_id, dict):
489                 self.pool = pool
490                 self.cr = cr
491                 self.uid = uid
492                 self.employee_id = employee_id
493                 self.dict = dict
494
495             def __getattr__(self, attr):
496                 return attr in self.dict and self.dict.__getitem__(attr) or 0.0
497
498         class InputLine(BrowsableObject):
499             """a class that will be used into the python code, mainly for usability purposes"""
500             def sum(self, code, from_date, to_date=None):
501                 if to_date is None:
502                     to_date = datetime.now().strftime('%Y-%m-%d')
503                 result = 0.0
504                 self.cr.execute("SELECT sum(amount) as sum\
505                             FROM hr_payslip as hp, hr_payslip_input as pi \
506                             WHERE hp.employee_id = %s AND hp.state = 'done' \
507                             AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s",
508                            (self.employee_id, from_date, to_date, code))
509                 res = self.cr.fetchone()[0]
510                 return res or 0.0
511
512         class WorkedDays(BrowsableObject):
513             """a class that will be used into the python code, mainly for usability purposes"""
514             def _sum(self, code, from_date, to_date=None):
515                 if to_date is None:
516                     to_date = datetime.now().strftime('%Y-%m-%d')
517                 result = 0.0
518                 self.cr.execute("SELECT sum(number_of_days) as number_of_days, sum(number_of_hours) as number_of_hours\
519                             FROM hr_payslip as hp, hr_payslip_worked_days as pi \
520                             WHERE hp.employee_id = %s AND hp.state = 'done'\
521                             AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s",
522                            (self.employee_id, from_date, to_date, code))
523                 return self.cr.fetchone()
524
525             def sum(self, code, from_date, to_date=None):
526                 res = self._sum(code, from_date, to_date)
527                 return res and res[0] or 0.0
528
529             def sum_hours(self, code, from_date, to_date=None):
530                 res = self._sum(code, from_date, to_date)
531                 return res and res[1] or 0.0
532
533         class Payslips(BrowsableObject):
534             """a class that will be used into the python code, mainly for usability purposes"""
535
536             def sum(self, code, from_date, to_date=None):
537                 if to_date is None:
538                     to_date = datetime.now().strftime('%Y-%m-%d')
539                 self.cr.execute("SELECT sum(case when hp.credit_note = False then (pl.total) else (-pl.total) end)\
540                             FROM hr_payslip as hp, hr_payslip_line as pl \
541                             WHERE hp.employee_id = %s AND hp.state = 'done' \
542                             AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id AND pl.code = %s",
543                             (self.employee_id, from_date, to_date, code))
544                 res = self.cr.fetchone()
545                 return res and res[0] or 0.0
546
547         #we keep a dict with the result because a value can be overwritten by another rule with the same code
548         result_dict = {}
549         rules = {}
550         categories_dict = {}
551         blacklist = []
552         payslip_obj = self.pool.get('hr.payslip')
553         inputs_obj = self.pool.get('hr.payslip.worked_days')
554         obj_rule = self.pool.get('hr.salary.rule')
555         payslip = payslip_obj.browse(cr, uid, payslip_id, context=context)
556         worked_days = {}
557         for worked_days_line in payslip.worked_days_line_ids:
558             worked_days[worked_days_line.code] = worked_days_line
559         inputs = {}
560         for input_line in payslip.input_line_ids:
561             inputs[input_line.code] = input_line
562
563         categories_obj = BrowsableObject(self.pool, cr, uid, payslip.employee_id.id, categories_dict)
564         input_obj = InputLine(self.pool, cr, uid, payslip.employee_id.id, inputs)
565         worked_days_obj = WorkedDays(self.pool, cr, uid, payslip.employee_id.id, worked_days)
566         payslip_obj = Payslips(self.pool, cr, uid, payslip.employee_id.id, payslip)
567         rules_obj = BrowsableObject(self.pool, cr, uid, payslip.employee_id.id, rules)
568
569         localdict = {'categories': categories_obj, 'rules': rules_obj, 'payslip': payslip_obj, 'worked_days': worked_days_obj, 'inputs': input_obj}
570         #get the ids of the structures on the contracts and their parent id as well
571         structure_ids = self.pool.get('hr.contract').get_all_structures(cr, uid, contract_ids, context=context)
572         #get the rules of the structure and thier children
573         rule_ids = self.pool.get('hr.payroll.structure').get_all_rules(cr, uid, structure_ids, context=context)
574         #run the rules by sequence
575         sorted_rule_ids = [id for id, sequence in sorted(rule_ids, key=lambda x:x[1])]
576
577         for contract in self.pool.get('hr.contract').browse(cr, uid, contract_ids, context=context):
578             employee = contract.employee_id
579             localdict.update({'employee': employee, 'contract': contract})
580             for rule in obj_rule.browse(cr, uid, sorted_rule_ids, context=context):
581                 key = rule.code + '-' + str(contract.id)
582                 localdict['result'] = None
583                 localdict['result_qty'] = 1.0
584                 #check if the rule can be applied
585                 if obj_rule.satisfy_condition(cr, uid, rule.id, localdict, context=context) and rule.id not in blacklist:
586                     #compute the amount of the rule
587                     amount, qty, rate = obj_rule.compute_rule(cr, uid, rule.id, localdict, context=context)
588                     #check if there is already a rule computed with that code
589                     previous_amount = rule.code in localdict and localdict[rule.code] or 0.0
590                     #set/overwrite the amount computed for this rule in the localdict
591                     tot_rule = amount * qty * rate / 100.0
592                     localdict[rule.code] = tot_rule
593                     rules[rule.code] = rule
594                     #sum the amount for its salary category
595                     localdict = _sum_salary_rule_category(localdict, rule.category_id, tot_rule - previous_amount)
596                     #create/overwrite the rule in the temporary results
597                     result_dict[key] = {
598                         'salary_rule_id': rule.id,
599                         'contract_id': contract.id,
600                         'name': rule.name,
601                         'code': rule.code,
602                         'category_id': rule.category_id.id,
603                         'sequence': rule.sequence,
604                         'appears_on_payslip': rule.appears_on_payslip,
605                         'condition_select': rule.condition_select,
606                         'condition_python': rule.condition_python,
607                         'condition_range': rule.condition_range,
608                         'condition_range_min': rule.condition_range_min,
609                         'condition_range_max': rule.condition_range_max,
610                         'amount_select': rule.amount_select,
611                         'amount_fix': rule.amount_fix,
612                         'amount_python_compute': rule.amount_python_compute,
613                         'amount_percentage': rule.amount_percentage,
614                         'amount_percentage_base': rule.amount_percentage_base,
615                         'register_id': rule.register_id.id,
616                         'amount': amount,
617                         'employee_id': contract.employee_id.id,
618                         'quantity': qty,
619                         'rate': rate,
620                     }
621                 else:
622                     #blacklist this rule and its children
623                     blacklist += [id for id, seq in self.pool.get('hr.salary.rule')._recursive_search_of_rules(cr, uid, [rule], context=context)]
624
625         result = [value for code, value in result_dict.items()]
626         return result
627
628     def onchange_employee_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
629         empolyee_obj = self.pool.get('hr.employee')
630         contract_obj = self.pool.get('hr.contract')
631         worked_days_obj = self.pool.get('hr.payslip.worked_days')
632         input_obj = self.pool.get('hr.payslip.input')
633
634         if context is None:
635             context = {}
636         #delete old worked days lines
637         old_worked_days_ids = ids and worked_days_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
638         if old_worked_days_ids:
639             worked_days_obj.unlink(cr, uid, old_worked_days_ids, context=context)
640
641         #delete old input lines
642         old_input_ids = ids and input_obj.search(cr, uid, [('payslip_id', '=', ids[0])], context=context) or False
643         if old_input_ids:
644             input_obj.unlink(cr, uid, old_input_ids, context=context)
645
646
647         #defaults
648         res = {'value':{
649                       'line_ids':[],
650                       'input_line_ids': [],
651                       'worked_days_line_ids': [],
652                       #'details_by_salary_head':[], TODO put me back
653                       'name':'',
654                       'contract_id': False,
655                       'struct_id': False,
656                       }
657             }
658         if (not employee_id) or (not date_from) or (not date_to):
659             return res
660         ttyme = datetime.fromtimestamp(time.mktime(time.strptime(date_from, "%Y-%m-%d")))
661         employee_id = empolyee_obj.browse(cr, uid, employee_id, context=context)
662         res['value'].update({
663                     'name': _('Salary Slip of %s for %s') % (employee_id.name, tools.ustr(ttyme.strftime('%B-%Y'))),
664                     'company_id': employee_id.company_id.id
665         })
666
667         if not context.get('contract', False):
668             #fill with the first contract of the employee
669             contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
670         else:
671             if contract_id:
672                 #set the list of contract for which the input have to be filled
673                 contract_ids = [contract_id]
674             else:
675                 #if we don't give the contract, then the input to fill should be for all current contracts of the employee
676                 contract_ids = self.get_contract(cr, uid, employee_id, date_from, date_to, context=context)
677
678         if not contract_ids:
679             return res
680         contract_record = contract_obj.browse(cr, uid, contract_ids[0], context=context)
681         res['value'].update({
682                     'contract_id': contract_record and contract_record.id or False
683         })
684         struct_record = contract_record and contract_record.struct_id or False
685         if not struct_record:
686             return res
687         res['value'].update({
688                     'struct_id': struct_record.id,
689         })
690         #computation of the salary input
691         worked_days_line_ids = self.get_worked_day_lines(cr, uid, contract_ids, date_from, date_to, context=context)
692         input_line_ids = self.get_inputs(cr, uid, contract_ids, date_from, date_to, context=context)
693         res['value'].update({
694                     'worked_days_line_ids': worked_days_line_ids,
695                     'input_line_ids': input_line_ids,
696         })
697         return res
698
699     def onchange_contract_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None):
700 #TODO it seems to be the mess in the onchanges, we should have onchange_employee => onchange_contract => doing all the things
701         if context is None:
702             context = {}
703         res = {'value':{
704                  'line_ids': [],
705                  'name': '',
706                  }
707               }
708         context.update({'contract': True})
709         if not contract_id:
710             res['value'].update({'struct_id': False})
711         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)
712
713 hr_payslip()
714
715 class hr_payslip_worked_days(osv.osv):
716     '''
717     Payslip Worked Days
718     '''
719
720     _name = 'hr.payslip.worked_days'
721     _description = 'Payslip Worked Days'
722     _columns = {
723         'name': fields.char('Description', size=256, required=True),
724         'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade', select=True),
725         'sequence': fields.integer('Sequence', required=True, select=True),
726         'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
727         'number_of_days': fields.float('Number of Days'),
728         'number_of_hours': fields.float('Number of Hours'),
729         'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
730     }
731     _order = 'payslip_id, sequence'
732     _defaults = {
733         'sequence': 10,
734     }
735 hr_payslip_worked_days()
736
737 class hr_payslip_input(osv.osv):
738     '''
739     Payslip Input
740     '''
741
742     _name = 'hr.payslip.input'
743     _description = 'Payslip Input'
744     _columns = {
745         'name': fields.char('Description', size=256, required=True),
746         'payslip_id': fields.many2one('hr.payslip', 'Pay Slip', required=True, ondelete='cascade', select=True),
747         'sequence': fields.integer('Sequence', required=True, select=True),
748         'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
749         '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."),
750         'contract_id': fields.many2one('hr.contract', 'Contract', required=True, help="The contract for which applied this input"),
751     }
752     _order = 'payslip_id, sequence'
753     _defaults = {
754         'sequence': 10,
755         'amount': 0.0,
756     }
757
758 hr_payslip_input()
759
760 class hr_salary_rule(osv.osv):
761
762     _name = 'hr.salary.rule'
763     _columns = {
764         'name':fields.char('Name', size=256, required=True, readonly=False),
765         '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."),
766         'sequence': fields.integer('Sequence', required=True, help='Use to arrange calculation sequence', select=True),
767         '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."),
768         'category_id':fields.many2one('hr.salary.rule.category', 'Category', required=True),
769         '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."),
770         'appears_on_payslip': fields.boolean('Appears on Payslip', help="Used to display the salary rule on payslip."),
771         'parent_rule_id':fields.many2one('hr.salary.rule', 'Parent Salary Rule', select=True),
772         'company_id':fields.many2one('res.company', 'Company', required=False),
773         'condition_select': fields.selection([('none', 'Always True'),('range', 'Range'), ('python', 'Python Expression')], "Condition Based on", required=True),
774         '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.'),
775         '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.'),
776         'condition_range_min': fields.float('Minimum Range', required=False, help="The minimum amount, applied for this rule."),
777         'condition_range_max': fields.float('Maximum Range', required=False, help="The maximum amount, applied for this rule."),
778         'amount_select':fields.selection([
779             ('percentage','Percentage (%)'),
780             ('fix','Fixed Amount'),
781             ('code','Python Code'),
782         ],'Amount Type', select=True, required=True, help="The computation method for the rule amount."),
783         'amount_fix': fields.float('Fixed Amount', digits_compute=dp.get_precision('Payroll'),),
784         'amount_percentage': fields.float('Percentage (%)', digits_compute=dp.get_precision('Payroll Rate'), help='For example, enter 50.0 to apply a percentage of 50%'),
785         'amount_python_compute':fields.text('Python Code'),
786         'amount_percentage_base':fields.char('Percentage based on',size=1024, required=False, readonly=False, help='result will be affected to a variable'),
787         'child_ids':fields.one2many('hr.salary.rule', 'parent_rule_id', 'Child Salary Rule'),
788         'register_id':fields.many2one('hr.contribution.register', 'Contribution Register', help="Eventual third party involved in the salary payment of the employees."),
789         'input_ids': fields.one2many('hr.rule.input', 'input_id', 'Inputs'),
790         'note':fields.text('Description'),
791      }
792     _defaults = {
793         'amount_python_compute': '''
794 # Available variables:
795 #----------------------
796 # payslip: object containing the payslips
797 # employee: hr.employee object
798 # contract: hr.contract object
799 # rules: object containing the rules code (previously computed)
800 # categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category).
801 # worked_days: object containing the computed worked days.
802 # inputs: object containing the computed inputs.
803
804 # Note: returned value have to be set in the variable 'result'
805
806 result = contract.wage * 0.10''',
807         'condition_python':
808 '''
809 # Available variables:
810 #----------------------
811 # payslip: object containing the payslips
812 # employee: hr.employee object
813 # contract: hr.contract object
814 # rules: object containing the rules code (previously computed)
815 # categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category).
816 # worked_days: object containing the computed worked days
817 # inputs: object containing the computed inputs
818
819 # Note: returned value have to be set in the variable 'result'
820
821 result = rules.NET > categories.NET * 0.10''',
822         'condition_range': 'contract.wage',
823         'sequence': 5,
824         'appears_on_payslip': True,
825         'active': True,
826         'company_id': lambda self, cr, uid, context: \
827                 self.pool.get('res.users').browse(cr, uid, uid,
828                     context=context).company_id.id,
829         'condition_select': 'none',
830         'amount_select': 'fix',
831         'amount_fix': 0.0,
832         'amount_percentage': 0.0,
833         'quantity': '1.0',
834      }
835
836     def _recursive_search_of_rules(self, cr, uid, rule_ids, context=None):
837         """
838         @param rule_ids: list of browse record
839         @return: returns a list of tuple (id, sequence) which are all the children of the passed rule_ids
840         """
841         children_rules = []
842         for rule in rule_ids:
843             if rule.child_ids:
844                 children_rules += self._recursive_search_of_rules(cr, uid, rule.child_ids, context=context)
845         return [(r.id, r.sequence) for r in rule_ids] + children_rules
846
847     #TODO should add some checks on the type of result (should be float)
848     def compute_rule(self, cr, uid, rule_id, localdict, context=None):
849         """
850         :param rule_id: id of rule to compute
851         :param localdict: dictionary containing the environement in which to compute the rule
852         :return: returns a tuple build as the base/amount computed, the quantity and the rate
853         :rtype: (float, float, float)
854         """
855         rule = self.browse(cr, uid, rule_id, context=context)
856         if rule.amount_select == 'fix':
857             try:
858                 return rule.amount_fix, eval(rule.quantity, localdict), 100.0
859             except:
860                 raise osv.except_osv(_('Error!'), _('Wrong quantity defined for salary rule %s (%s).')% (rule.name, rule.code))
861         elif rule.amount_select == 'percentage':
862             try:
863                 return eval(rule.amount_percentage_base, localdict), eval(rule.quantity, localdict), rule.amount_percentage
864             except:
865                 raise osv.except_osv(_('Error!'), _('Wrong percentage base or quantity defined for salary rule %s (%s).')% (rule.name, rule.code))
866         else:
867             try:
868                 eval(rule.amount_python_compute, localdict, mode='exec', nocopy=True)
869                 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
870             except:
871                 raise osv.except_osv(_('Error!'), _('Wrong python code defined for salary rule %s (%s).')% (rule.name, rule.code))
872
873     def satisfy_condition(self, cr, uid, rule_id, localdict, context=None):
874         """
875         @param rule_id: id of hr.salary.rule to be tested
876         @param contract_id: id of hr.contract to be tested
877         @return: returns True if the given rule match the condition for the given contract. Return False otherwise.
878         """
879         rule = self.browse(cr, uid, rule_id, context=context)
880
881         if rule.condition_select == 'none':
882             return True
883         elif rule.condition_select == 'range':
884             try:
885                 result = eval(rule.condition_range, localdict)
886                 return rule.condition_range_min <=  result and result <= rule.condition_range_max or False
887             except:
888                 raise osv.except_osv(_('Error!'), _('Wrong range condition defined for salary rule %s (%s).')% (rule.name, rule.code))
889         else: #python code
890             try:
891                 eval(rule.condition_python, localdict, mode='exec', nocopy=True)
892                 return 'result' in localdict and localdict['result'] or False
893             except:
894                 raise osv.except_osv(_('Error!'), _('Wrong python condition defined for salary rule %s (%s).')% (rule.name, rule.code))
895
896 hr_salary_rule()
897
898 class hr_rule_input(osv.osv):
899     '''
900     Salary Rule Input
901     '''
902
903     _name = 'hr.rule.input'
904     _description = 'Salary Rule Input'
905     _columns = {
906         'name': fields.char('Description', size=256, required=True),
907         'code': fields.char('Code', size=52, required=True, help="The code that can be used in the salary rules"),
908         'input_id': fields.many2one('hr.salary.rule', 'Salary Rule Input', required=True)
909     }
910
911 hr_rule_input()
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 hr_payslip_line()
947
948 class hr_payroll_structure(osv.osv):
949
950     _inherit = 'hr.payroll.structure'
951     _columns = {
952         'rule_ids':fields.many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id', 'Salary Rules'),
953     }
954
955 hr_payroll_structure()
956
957 class hr_employee(osv.osv):
958     '''
959     Employee
960     '''
961
962     _inherit = 'hr.employee'
963     _description = 'Employee'
964
965     def _calculate_total_wage(self, cr, uid, ids, name, args, context):
966         if not ids: return {}
967         res = {}
968         current_date = datetime.now().strftime('%Y-%m-%d')
969         for employee in self.browse(cr, uid, ids, context=context):
970             if not employee.contract_ids:
971                 res[employee.id] = {'basic': 0.0}
972                 continue
973             cr.execute( 'SELECT SUM(wage) '\
974                         'FROM hr_contract '\
975                         'WHERE employee_id = %s '\
976                         'AND date_start <= %s '\
977                         'AND (date_end > %s OR date_end is NULL)',
978                          (employee.id, current_date, current_date))
979             result = dict(cr.dictfetchone())
980             res[employee.id] = {'basic': result['sum']}
981         return res
982
983     _columns = {
984         'slip_ids':fields.one2many('hr.payslip', 'employee_id', 'Payslips', required=False, readonly=True),
985         '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."),
986     }
987
988 hr_employee()
989
990 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: