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 expense, so it's valid
71
if line.account_id and line.account_id.user_type_code and line.account_id.user_type_code != 'expense':
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
158
'date': fields.date(string='Date', required=True, readonly=True),
159
'document_date': fields.date(string='Document Date', required=True, readonly=True),
160
'account_id': fields.many2one('account.account', string="Account", required=True, readonly=True),
161
'period_id': fields.many2one('account.period', string="Period", required=True, readonly=True),
162
'employee_id': fields.many2one('hr.employee', string="Employee", readonly=True, ondelete="restrict"),
163
'partner_id': fields.many2one('res.partner', string="Partner", readonly=True, ondelete="restrict"),
164
'journal_id': fields.many2one('account.journal', string="Journal", readonly=True, ondelete="restrict"),
165
'employee_id_number': fields.function(_get_employee_identification_id, method=True, type='char', size=255, string='Employee ID', readonly=True),
166
'name': fields.char(string='Description', size=255, readonly=True),
167
'ref': fields.char(string='Reference', size=255, readonly=True),
168
'amount': fields.float(string='Amount', digits_compute=get_precision('Account'), readonly=True),
169
'currency_id': fields.many2one('res.currency', string="Currency", required=True, readonly=True),
170
'state': fields.selection([('draft', 'Draft'), ('valid', 'Validated')], string="State", required=True, readonly=True),
171
'cost_center_id': fields.many2one('account.analytic.account', string="Cost Center", required=False, domain="[('category','=','OC'), ('type', '!=', 'view'), ('state', '=', 'open')]"),
172
'funding_pool_id': fields.many2one('account.analytic.account', string="Funding Pool", domain="[('category', '=', 'FUNDING'), ('type', '!=', 'view'), ('state', '=', 'open')]"),
173
'free1_id': fields.many2one('account.analytic.account', string="Free 1", domain="[('category', '=', 'FREE1'), ('type', '!=', 'view'), ('state', '=', 'open')]"),
174
'free2_id': fields.many2one('account.analytic.account', string="Free 2", domain="[('category', '=', 'FREE2'), ('type', '!=', 'view'), ('state', '=', 'open')]"),
175
'destination_id': fields.many2one('account.analytic.account', string="Destination", domain="[('category', '=', 'DEST'), ('type', '!=', 'view'), ('state', '=', 'open')]"),
176
'analytic_state': fields.function(_get_analytic_state, type='selection', method=True, readonly=True, string="Distribution State",
177
selection=[('none', 'None'), ('valid', 'Valid'), ('invalid', 'Invalid')], help="Give analytic distribution state"),
178
'partner_type': fields.function(_get_third_parties, type='reference', method=True, string="Third Parties", readonly=True,
179
selection=[('res.partner', 'Partner'), ('account.journal', 'Journal'), ('hr.employee', 'Employee')]),
180
'field': fields.char(string='Field', readonly=True, size=255, help="Field this line come from in Homère."),
183
_order = 'employee_id, date desc'
186
'date': lambda *a: strftime('%Y-%m-%d'),
187
'document_date': lambda *a: strftime('%Y-%m-%d'),
188
'state': lambda *a: 'draft',
191
def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
193
Change funding pool domain in order to include MSF Private fund
197
view = super(hr_payroll, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu)
198
if view_type in ['tree', 'form']:
199
form = etree.fromstring(view['arch'])
200
data_obj = self.pool.get('ir.model.data')
202
oc_id = data_obj.get_object_reference(cr, uid, 'analytic_distribution', 'analytic_account_project')[1]
206
fields = form.xpath('//field[@name="cost_center_id"]')
208
field.set('domain', "[('category', '=', 'OC'), ('type', '!=', 'view'), ('state', '=', 'open'), ('id', 'child_of', [%s])]" % oc_id)
211
fp_id = data_obj.get_object_reference(cr, uid, 'analytic_distribution', 'analytic_account_msf_private_funds')[1]
214
fp_fields = form.xpath('//field[@name="funding_pool_id"]')
215
for field in fp_fields:
216
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)
217
# Change Destination field
218
dest_fields = form.xpath('//field[@name="destination_id"]')
219
for field in dest_fields:
220
field.set('domain', "[('type', '!=', 'view'), ('state', '=', 'open'), ('category', '=', 'DEST'), ('destination_ids', '=', account_id)]")
222
view['arch'] = etree.tostring(form)
225
def onchange_destination(self, cr, uid, ids, destination_id=False, funding_pool_id=False, account_id=False):
227
Check given funding pool with destination
229
# Prepare some values
231
# If all elements given, then search FP compatibility
232
if destination_id and funding_pool_id and account_id:
233
fp_line = self.pool.get('account.analytic.account').browse(cr, uid, funding_pool_id)
234
# Search MSF Private Fund element, because it's valid with all accounts
236
fp_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'analytic_distribution',
237
'analytic_account_msf_private_funds')[1]
240
# Delete funding_pool_id if not valid with tuple "account_id/destination_id".
241
# but do an exception for MSF Private FUND analytic account
242
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:
243
res = {'value': {'funding_pool_id': False}}
244
# If no destination, do nothing
245
elif not destination_id:
247
# Otherway: delete FP
249
res = {'value': {'funding_pool_id': False}}
250
# If destination given, search if given
253
def create(self, cr, uid, vals, context=None):
255
Raise an error if creation don't become from an import or a YAML.
256
Add default analytic distribution for those that doesn't have anyone.
260
if not context.get('from', False) and not context.get('from') in ['yaml', 'csv_import']:
261
raise osv.except_osv(_('Error'), _('You are not able to create payroll entries.'))
262
if not vals.get('funding_pool_id', False):
264
fp_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'analytic_distribution', 'analytic_account_msf_private_funds')[1]
268
vals.update({'funding_pool_id': fp_id,})
269
return super(osv.osv, self).create(cr, uid, vals, context)
272
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: