2
# -*- coding: utf-8 -*-
3
##############################################################################
5
# OpenERP, Open Source Management Solution
6
# Copyright (C) 2012 TeMPO Consulting, MSF. All Rights Reserved
7
# Developer: Olivier DOSSMANN
9
# This program is free software: you can redistribute it and/or modify
10
# it under the terms of the GNU Affero General Public License as
11
# published by the Free Software Foundation, either version 3 of the
12
# License, or (at your option) any later version.
14
# This program is distributed in the hope that it will be useful,
15
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
# GNU Affero General Public License for more details.
19
# You should have received a copy of the GNU Affero General Public License
20
# along with this program. If not, see <http://www.gnu.org/licenses/>.
22
##############################################################################
25
from osv import fields
26
from decimal_precision import get_precision
27
from time import strftime
28
from lxml import etree
29
from tools.translate import _
31
class hr_payroll(osv.osv):
32
_name = 'hr.payroll.msf'
33
_description = 'Payroll'
35
def _get_analytic_state(self, cr, uid, ids, name, args, context=None):
37
Get state of distribution:
38
- if compatible with the line, then "valid"
39
- all other case are "invalid"
41
if isinstance(ids, (int, long)):
45
# Search MSF Private Fund element, because it's valid with all accounts
47
fp_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'analytic_distribution',
48
'analytic_account_msf_private_funds')[1]
51
# Browse all given lines to check analytic distribution validity
54
# B/ if FP = MSF Private FUND
55
# C/ (account/DEST) in FP except B
56
# D/ CC in FP except when B
57
# E/ DEST in list of available DEST in ACCOUNT
58
# F/ Check posting date with cost center and destination if exists
59
# G/ Check document date with funding pool
60
## CASES where FP is filled in (or not) and/or DEST is filled in (or not).
61
## CC is mandatory, so always available:
62
# 1/ no FP, no DEST => Distro = valid
63
# 2/ FP, no DEST => Check D except B
64
# 3/ no FP, DEST => Check E
65
# 4/ FP, DEST => Check C, D except B, E
67
for line in self.browse(cr, uid, ids, context=context):
68
res[line.id] = 'valid' # by default
69
#### SOME CASE WHERE DISTRO IS OK
70
# if account is not analytic-a-holic, so it's valid
71
if line.account_id and not line.account_id.is_analytic_addicted:
75
if line.cost_center_id:
76
cc = self.pool.get('account.analytic.account').browse(cr, uid, line.cost_center_id.id, context={'date': line.date})
77
if cc and cc.filter_active is False:
78
res[line.id] = 'invalid'
80
if line.destination_id:
81
dest = self.pool.get('account.analytic.account').browse(cr, uid, line.destination_id.id, context={'date': line.date})
82
if dest and dest.filter_active is False:
83
res[line.id] = 'invalid'
86
if line.funding_pool_id:
87
fp = self.pool.get('account.analytic.account').browse(cr, uid, line.funding_pool_id.id, context={'date': line.document_date})
88
if fp and fp.filter_active is False:
89
res[line.id] = 'invalid'
91
# if just a cost center, it's also valid! (CASE 1/)
92
if not line.funding_pool_id and not line.destination_id:
94
# if FP is MSF Private Fund and no destination_id, then all is OK.
95
if line.funding_pool_id and line.funding_pool_id.id == fp_id and not line.destination_id:
98
# if no cost center, distro is invalid (CASE A/)
99
if not line.cost_center_id:
100
res[line.id] = 'invalid'
102
if line.funding_pool_id and not line.destination_id: # CASE 2/
103
# D Check, except B check
104
if line.cost_center_id.id not in [x.id for x in line.funding_pool_id.cost_center_ids] and line.funding_pool_id.id != fp_id:
105
res[line.id] = 'invalid'
107
elif not line.funding_pool_id and line.destination_id: # CASE 3/
109
account = self.pool.get('account.account').browse(cr, uid, line.account_id.id)
110
if line.destination_id.id not in [x.id for x in account.destination_ids]:
111
res[line.id] = 'invalid'
115
if (line.account_id.id, line.destination_id.id) not in [x.account_id and x.destination_id and (x.account_id.id, x.destination_id.id) for x in line.funding_pool_id.tuple_destination_account_ids] and line.funding_pool_id.id != fp_id:
116
res[line.id] = 'invalid'
118
# D Check, except B check
119
if line.cost_center_id.id not in [x.id for x in line.funding_pool_id.cost_center_ids] and line.funding_pool_id.id != fp_id:
120
res[line.id] = 'invalid'
123
account = self.pool.get('account.account').browse(cr, uid, line.account_id.id)
124
if line.destination_id.id not in [x.id for x in account.destination_ids]:
125
res[line.id] = 'invalid'
129
def _get_third_parties(self, cr, uid, ids, field_name=None, arg=None, context=None):
131
Get "Third Parties" following other fields
134
for line in self.browse(cr, uid, ids):
136
res[line.id] = {'third_parties': 'hr.employee,%s' % line.employee_id.id}
137
res[line.id] = 'hr.employee,%s' % line.employee_id.id
138
elif line.journal_id:
139
res[line.id] = 'account.journal,%s' % line.transfer_journal_id.id
140
elif line.partner_id:
141
res[line.id] = 'res.partner,%s' % line.partner_id.id
146
def _get_employee_identification_id(self, cr, uid, ids, field_name=None, arg=None, context=None):
148
Get employee identification number if employee id is given
151
for line in self.browse(cr, uid, ids):
154
res[line.id] = line.employee_id.identification_id
157
def _get_trigger_state_ana(self, cr, uid, ids, context=None):
158
if isinstance(ids, (int, long)):
164
for ana_account in self.read(cr, uid, ids, ['category']):
165
if ana_account['category'] == 'OC':
166
cc.append(ana_account['id'])
167
elif ana_account['category'] == 'DEST':
168
dest.append(ana_account['id'])
169
elif ana_account['category'] == 'FUNDING':
170
fp.append(ana_account['id'])
171
if len(fp) > 1 or len(cc) > 1 or len(dest) > 1:
172
return self.pool.get('hr.payroll.msf').search(cr, uid, [('state', '=', 'draft'), '|', '|', ('funding_pool_id', 'in', fp), ('cost_center_id','in', cc), ('destination_id','in', dest)])
176
def _get_trigger_state_account(self, cr, uid, ids, context=None):
177
pay_obj = self.pool.get('hr.payroll.msf')
178
return pay_obj.search(cr, uid, [('state', '=', 'draft'), ('account_id', 'in', ids)])
180
def _get_trigger_state_dest_link(self, cr, uid, ids, context=None):
181
if isinstance(ids, (int, long)):
184
pay_obj = self.pool.get('hr.payroll.msf')
185
for dest_link in self.read(cr, uid, ids, ['account_id', 'destination_id', 'funding_pool_ids']):
186
to_update += pay_obj.search(cr, uid, [
187
('state', '=', 'draft'),
188
('account_id', '=', dest_link['account_id'][0]),
189
('destination_id', '=', dest_link['destination_id'][0]),
190
('funding_pool_id', 'in', dest_link['funding_pool_ids'])
195
'date': fields.date(string='Date', required=True, readonly=True),
196
'document_date': fields.date(string='Document Date', required=True, readonly=True),
197
'account_id': fields.many2one('account.account', string="Account", required=True, readonly=True),
198
'period_id': fields.many2one('account.period', string="Period", required=True, readonly=True),
199
'employee_id': fields.many2one('hr.employee', string="Employee", readonly=True, ondelete="restrict"),
200
'partner_id': fields.many2one('res.partner', string="Partner", readonly=True, ondelete="restrict"),
201
'journal_id': fields.many2one('account.journal', string="Journal", readonly=True, ondelete="restrict"),
202
'employee_id_number': fields.function(_get_employee_identification_id, method=True, type='char', size=255, string='Employee ID', readonly=True),
203
'name': fields.char(string='Description', size=255, readonly=True),
204
'ref': fields.char(string='Reference', size=255, readonly=True),
205
'amount': fields.float(string='Amount', digits_compute=get_precision('Account'), readonly=True),
206
'currency_id': fields.many2one('res.currency', string="Currency", required=True, readonly=True),
207
'state': fields.selection([('draft', 'Draft'), ('valid', 'Validated')], string="State", required=True, readonly=True),
208
'cost_center_id': fields.many2one('account.analytic.account', string="Cost Center", required=False, domain="[('category','=','OC'), ('type', '!=', 'view'), ('state', '=', 'open')]"),
209
'funding_pool_id': fields.many2one('account.analytic.account', string="Funding Pool", domain="[('category', '=', 'FUNDING'), ('type', '!=', 'view'), ('state', '=', 'open')]"),
210
'free1_id': fields.many2one('account.analytic.account', string="Free 1", domain="[('category', '=', 'FREE1'), ('type', '!=', 'view'), ('state', '=', 'open')]"),
211
'free2_id': fields.many2one('account.analytic.account', string="Free 2", domain="[('category', '=', 'FREE2'), ('type', '!=', 'view'), ('state', '=', 'open')]"),
212
'destination_id': fields.many2one('account.analytic.account', string="Destination", domain="[('category', '=', 'DEST'), ('type', '!=', 'view'), ('state', '=', 'open')]"),
213
'analytic_state': fields.function(_get_analytic_state, type='selection', method=True, readonly=True, string="Distribution State",
214
selection=[('none', 'None'), ('valid', 'Valid'), ('invalid', 'Invalid')], help="Give analytic distribution state",
216
'hr.payroll.msf': (lambda self, cr, uid, ids, c=None: ids, ['account_id', 'cost_center_id', 'funding_pool_id', 'destination_id'], 10),
217
'account.account': (_get_trigger_state_account, ['user_type_code', 'destination_ids'], 20),
218
'account.analytic.account': (_get_trigger_state_ana, ['date', 'date_start', 'cost_center_ids', 'tuple_destination_account_ids'], 20),
219
'account.destination.link': (_get_trigger_state_dest_link, ['account_id', 'destination_id'], 30),
222
'partner_type': fields.function(_get_third_parties, type='reference', method=True, string="Third Parties", readonly=True,
223
selection=[('res.partner', 'Partner'), ('account.journal', 'Journal'), ('hr.employee', 'Employee')]),
224
'field': fields.char(string='Field', readonly=True, size=255, help="Field this line come from in Homère."),
227
_order = 'employee_id, date desc'
230
'date': lambda *a: strftime('%Y-%m-%d'),
231
'document_date': lambda *a: strftime('%Y-%m-%d'),
232
'state': lambda *a: 'draft',
235
def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
237
Change funding pool domain in order to include MSF Private fund
241
view = super(hr_payroll, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu)
242
if view_type in ['tree', 'form']:
243
form = etree.fromstring(view['arch'])
244
data_obj = self.pool.get('ir.model.data')
246
oc_id = data_obj.get_object_reference(cr, uid, 'analytic_distribution', 'analytic_account_project')[1]
250
fields = form.xpath('//field[@name="cost_center_id"]')
252
field.set('domain', "[('category', '=', 'OC'), ('type', '!=', 'view'), ('state', '=', 'open'), ('id', 'child_of', [%s])]" % oc_id)
255
fp_id = data_obj.get_object_reference(cr, uid, 'analytic_distribution', 'analytic_account_msf_private_funds')[1]
258
fp_fields = form.xpath('//field[@name="funding_pool_id"]')
259
for field in fp_fields:
260
field.set('domain', "[('type', '!=', 'view'), ('state', '=', 'open'), ('category', '=', 'FUNDING'), '|', '&', ('cost_center_ids', '=', cost_center_id), ('tuple_destination', '=', (account_id, destination_id)), ('id', '=', %s)]" % fp_id)
261
# Change Destination field
262
dest_fields = form.xpath('//field[@name="destination_id"]')
263
for field in dest_fields:
264
field.set('domain', "[('type', '!=', 'view'), ('state', '=', 'open'), ('category', '=', 'DEST'), ('destination_ids', '=', account_id)]")
266
view['arch'] = etree.tostring(form)
269
def onchange_destination(self, cr, uid, ids, destination_id=False, funding_pool_id=False, account_id=False):
271
Check given funding pool with destination
273
# Prepare some values
275
# If all elements given, then search FP compatibility
276
if destination_id and funding_pool_id and account_id:
277
fp_line = self.pool.get('account.analytic.account').browse(cr, uid, funding_pool_id)
278
# Search MSF Private Fund element, because it's valid with all accounts
280
fp_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'analytic_distribution',
281
'analytic_account_msf_private_funds')[1]
284
# Delete funding_pool_id if not valid with tuple "account_id/destination_id".
285
# but do an exception for MSF Private FUND analytic account
286
if (account_id, destination_id) not in [x.account_id and x.destination_id and (x.account_id.id, x.destination_id.id) for x in fp_line.tuple_destination_account_ids] and funding_pool_id != fp_id:
287
res = {'value': {'funding_pool_id': False}}
288
# If no destination, do nothing
289
elif not destination_id:
291
# Otherway: delete FP
293
res = {'value': {'funding_pool_id': False}}
294
# If destination given, search if given
297
def create(self, cr, uid, vals, context=None):
299
Raise an error if creation don't become from an import or a YAML.
300
Add default analytic distribution for those that doesn't have anyone.
304
if not context.get('from', False) and not context.get('from') in ['yaml', 'csv_import']:
305
raise osv.except_osv(_('Error'), _('You are not able to create payroll entries.'))
306
if not vals.get('funding_pool_id', False):
308
fp_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'analytic_distribution', 'analytic_account_msf_private_funds')[1]
312
vals.update({'funding_pool_id': fp_id,})
313
return super(osv.osv, self).create(cr, uid, vals, context)
316
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: