1
# -*- coding: utf-8 -*-
2
##############################################################################
4
# OpenERP, Open Source Management Solution
5
# Copyright (C) 2011 MSF, TeMPO consulting
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU Affero General Public License as
9
# published by the Free Software Foundation, either version 3 of the
10
# License, or (at your option) any later version.
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU Affero General Public License for more details.
17
# You should have received a copy of the GNU Affero General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
20
##############################################################################
22
from osv import fields, osv
24
from tools.translate import _
26
class account_move_line(osv.osv):
27
_inherit = 'account.move.line'
29
def _display_analytic_button(self, cr, uid, ids, name, args, context=None):
31
Return True for all element that correspond to some criteria:
32
- The journal entry state is draft (unposted)
33
- The account is analytic-a-holic
36
for ml in self.browse(cr, uid, ids, context=context):
38
# False if account not anlaytic-a-holic
39
if not ml.account_id.is_analytic_addicted:
43
def _get_distribution_state(self, cr, uid, ids, name, args, context=None):
45
Get state of distribution:
46
- if compatible with the move line, then "valid"
47
- if no distribution, take a tour of move distribution, if compatible, then "valid"
48
- if no distribution on move line and move, then "none"
49
- all other case are "invalid"
54
if isinstance(ids, (int, long)):
58
# Browse all given lines
59
for line in self.browse(cr, uid, ids, context=context):
60
res[line.id] = self.pool.get('analytic.distribution')._get_distribution_state(cr, uid, line.analytic_distribution_id.id, line.move_id and line.move_id.analytic_distribution_id and line.move_id.analytic_distribution_id.id or False, line.account_id.id)
63
def _have_analytic_distribution_from_header(self, cr, uid, ids, name, arg, context=None):
65
If move have an analytic distribution, return False, else return True
70
if isinstance(ids, (int, long)):
73
for ml in self.browse(cr, uid, ids, context=context):
75
if ml.analytic_distribution_id:
79
def _get_distribution_state_recap(self, cr, uid, ids, name, arg, context=None):
81
Get a recap from analytic distribution state and if it come from header or not.
83
if isinstance(ids, (int, long)):
86
get_sel = self.pool.get('ir.model.fields').get_browse_selection
87
for ml in self.browse(cr, uid, ids):
90
if ml.have_analytic_distribution_from_header:
91
from_header = _(' (from header)')
92
d_state = get_sel(cr, uid, ml, 'analytic_distribution_state', context)
93
res[ml.id] = "%s%s" % (d_state, from_header)
94
# Do not show any recap for non analytic-a-holic accounts
95
if ml.account_id and not ml.account_id.is_analytic_addicted:
100
'analytic_distribution_id': fields.many2one('analytic.distribution', 'Analytic Distribution'),
101
'display_analytic_button': fields.function(_display_analytic_button, method=True, string='Display analytic button?', type='boolean', readonly=True,
102
help="This informs system that we can display or not an analytic button", store=False),
103
'analytic_distribution_state': fields.function(_get_distribution_state, method=True, type='selection',
104
selection=[('none', 'None'), ('valid', 'Valid'), ('invalid', 'Invalid')],
105
string="Distribution state", help="Informs from distribution state among 'none', 'valid', 'invalid."),
106
'have_analytic_distribution_from_header': fields.function(_have_analytic_distribution_from_header, method=True, type='boolean',
107
string='Header Distrib.?'),
108
'analytic_distribution_state_recap': fields.function(_get_distribution_state_recap, method=True, type='char', size=30,
109
string="Distribution",
110
help="Informs you about analaytic distribution state among 'none', 'valid', 'invalid', from header or not, or no analytic distribution"),
113
def create_analytic_lines(self, cr, uid, ids, context=None):
115
Create analytic lines on analytic-a-holic accounts that have an analytical distribution.
120
acc_ana_line_obj = self.pool.get('account.analytic.line')
121
company_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
122
for obj_line in self.browse(cr, uid, ids, context=context):
123
# Prepare some values
124
amount = obj_line.debit_currency - obj_line.credit_currency
125
line_distrib_id = obj_line.analytic_distribution_id and obj_line.analytic_distribution_id.id or obj_line.move_id and obj_line.move_id.analytic_distribution_id and obj_line.move_id.analytic_distribution_id.id or False
126
# When you create a journal entry manually, we should not have analytic lines if ONE line is invalid!
127
other_lines_are_ok = True
128
if obj_line.move_id and obj_line.move_id.status and obj_line.move_id.status == 'manu':
129
if obj_line.state != 'valid':
130
other_lines_are_ok = False
131
for other_line in obj_line.move_id.line_id:
132
if other_line.state != 'valid':
133
other_lines_are_ok = False
134
# Check that line have analytic-a-holic account and have a distribution
135
if line_distrib_id and obj_line.account_id.is_analytic_addicted and other_lines_are_ok:
136
ana_state = self.pool.get('analytic.distribution')._get_distribution_state(cr, uid, line_distrib_id, {}, obj_line.account_id.id)
137
# For manual journal entries, do not raise an error. But delete all analytic distribution linked to other_lines because if one line is invalid, all lines should not create analytic lines
138
if ana_state == 'invalid' and obj_line.move_id.status == 'manu':
139
ana_line_ids = acc_ana_line_obj.search(cr, uid, [('move_id', 'in', [x.id for x in obj_line.move_id.line_id])])
140
acc_ana_line_obj.unlink(cr, uid, ana_line_ids)
142
elif ana_state == 'invalid':
143
raise osv.except_osv(_('Warning'), _('Invalid analytic distribution.'))
144
if not obj_line.journal_id.analytic_journal_id:
145
raise osv.except_osv(_('Warning'),_("No Analytic Journal! You have to define an analytic journal on the '%s' journal!") % (obj_line.journal_id.name, ))
146
distrib_obj = self.pool.get('analytic.distribution').browse(cr, uid, line_distrib_id, context=context)
148
for distrib_lines in [distrib_obj.funding_pool_lines, distrib_obj.free_1_lines, distrib_obj.free_2_lines]:
149
for distrib_line in distrib_lines:
150
context.update({'date': obj_line.source_date or obj_line.date})
151
anal_amount = distrib_line.percentage*amount/100
153
'name': obj_line.name,
154
'date': obj_line.date,
156
'journal_id': obj_line.journal_id.analytic_journal_id.id,
157
'amount': -1 * self.pool.get('res.currency').compute(cr, uid, obj_line.currency_id.id, company_currency,
158
anal_amount, round=False, context=context),
159
'amount_currency': -1 * anal_amount,
160
'account_id': distrib_line.analytic_id.id,
161
'general_account_id': obj_line.account_id.id,
162
'move_id': obj_line.id,
163
'distribution_id': distrib_obj.id,
165
'currency_id': obj_line.currency_id.id,
166
'distrib_line_id': '%s,%s'%(distrib_line._name, distrib_line.id),
167
'document_date': obj_line.document_date,
169
# Update values if we come from a funding pool
170
if distrib_line._name == 'funding.pool.distribution.line':
171
destination_id = distrib_line.destination_id and distrib_line.destination_id.id or False
172
line_vals.update({'cost_center_id': distrib_line.cost_center_id and distrib_line.cost_center_id.id or False,
173
'destination_id': destination_id,})
174
# Update value if we come from a write-off
175
if obj_line.is_write_off:
176
line_vals.update({'from_write_off': True,})
177
# Add source_date value for account_move_line that are a correction of another account_move_line
178
if obj_line.corrected_line_id and obj_line.source_date:
179
line_vals.update({'source_date': obj_line.source_date})
180
self.pool.get('account.analytic.line').create(cr, uid, line_vals, context=context)
183
def unlink(self, cr, uid, ids, context=None, check=True):
185
Delete analytic lines before unlink move lines.
186
Update Manual Journal Entries.
192
for ml in self.browse(cr, uid, ids):
193
if ml.move_id and ml.move_id.state == 'manu':
194
move_ids.append(ml.move_id.id)
195
# Search analytic lines
196
ana_ids = self.pool.get('account.analytic.line').search(cr, uid, [('move_id', 'in', ids)])
197
self.pool.get('account.analytic.line').unlink(cr, uid, ana_ids)
199
self.pool.get('account.move').validate(cr, uid, move_ids)
200
return super(account_move_line, self).unlink(cr, uid, ids, context=context, check=check)
202
def button_analytic_distribution(self, cr, uid, ids, context=None):
204
Launch analytic distribution wizard on an move line
209
if isinstance(ids, (int, long)):
212
raise osv.except_osv(_('Error'), _('No journal item given. Please save your line before.'))
213
# Prepare some values
214
ml = self.browse(cr, uid, ids[0], context=context)
216
amount = ml.debit_currency - ml.credit_currency
217
# Search elements for currency
218
company_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
219
currency = ml.currency_id and ml.currency_id.id or company_currency
220
# Get analytic distribution id from this line
221
distrib_id = ml and ml.analytic_distribution_id and ml.analytic_distribution_id.id or False
222
# Prepare values for wizard
224
'total_amount': amount,
225
'move_line_id': ml.id,
226
'currency_id': currency or False,
228
'account_id': ml.account_id and ml.account_id.id or False,
229
'posting_date': ml.date,
230
'document_date': ml.document_date,
233
vals.update({'distribution_id': distrib_id,})
235
wiz_obj = self.pool.get('analytic.distribution.wizard')
236
wiz_id = wiz_obj.create(cr, uid, vals, context=context)
237
# Update some context values
244
'name': _('Analytic distribution'),
245
'type': 'ir.actions.act_window',
246
'res_model': 'analytic.distribution.wizard',
254
def _check_employee_analytic_distribution(self, cr, uid, ids, context=None):
256
Check that analytic distribution could be retrieved from given employee.
257
If not employee, return True.
261
if isinstance(ids, (int, long)):
263
for l in self.browse(cr, uid, ids):
264
# Next line if this one comes from a non-manual move (journal entry)
265
if l.move_id.status != 'manu':
267
# Do not continue if no employee or no cost center (could not be invented)
268
if not l.employee_id or not l.employee_id.cost_center_id:
270
if l.account_id and l.account_id.is_analytic_addicted:
271
vals = {'cost_center_id': l.employee_id.cost_center_id.id}
272
if l.employee_id.destination_id:
273
if l.employee_id.destination_id.id in [x and x.id for x in l.account_id.destination_ids]:
274
vals.update({'destination_id': l.employee_id.destination_id.id})
276
vals.update({'destination_id': l.account_id.default_destination_id.id})
277
if l.employee_id.funding_pool_id:
278
vals.update({'analytic_id': l.employee_id.funding_pool_id.id})
279
if vals.get('cost_center_id') not in l.employee_id.funding_pool_id.cost_center_ids:
280
# Fetch default funding pool: MSF Private Fund
282
msf_fp_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'analytic_distribution', 'analytic_account_msf_private_funds')[1]
285
vals.update({'analytic_id': msf_fp_id})
286
# Create analytic distribution
287
if 'cost_center_id' in vals and 'analytic_id' in vals and 'destination_id' in vals:
288
distrib_id = self.pool.get('analytic.distribution').create(cr, uid, {'name': 'check_employee_analytic_distribution'})
289
vals.update({'distribution_id': distrib_id, 'percentage': 100.0, 'currency_id': l.currency_id.id})
290
# Create funding pool lines
291
self.pool.get('funding.pool.distribution.line').create(cr, uid, vals)
292
# Then cost center lines
293
vals.update({'analytic_id': vals.get('cost_center_id'),})
294
self.pool.get('cost.center.distribution.line').create(cr, uid, vals)
295
# finally free1 and free2
296
if l.employee_id.free1_id:
297
self.pool.get('free.1.distribution.line').create(cr, uid, {'distribution_id': distrib_id, 'percentage': 100.0, 'currency_id': l.currency_id.id, 'analytic_id': l.employee_id.free1_id.id})
298
if l.employee_id.free2_id:
299
self.pool.get('free.2.distribution.line').create(cr, uid, {'distribution_id': distrib_id, 'percentage': 100.0, 'currency_id': l.currency_id.id, 'analytic_id': l.employee_id.free2_id.id})
300
if context.get('from_write', False):
301
return {'analytic_distribution_id': distrib_id,}
302
# Write analytic distribution on the move line
303
self.pool.get('account.move.line').write(cr, uid, [l.id], {'analytic_distribution_id': distrib_id}, check=False, update_check=False)
308
def create(self, cr, uid, vals, context=None, check=True):
310
Check analytic distribution for employee (if given)
312
res = super(account_move_line, self).create(cr, uid, vals, context, check)
313
self._check_employee_analytic_distribution(cr, uid, res, context)
316
def write(self, cr, uid, ids, vals, context=None, check=True, update_check=True):
318
Check line if we come from web (from_web_menu)
322
if isinstance(ids, (int, long)):
324
if context.get('from_web_menu', False):
326
for ml in self.browse(cr, uid, ids):
327
distrib_state = self.pool.get('analytic.distribution')._get_distribution_state(cr, uid, ml.analytic_distribution_id.id, ml.move_id and ml.move_id.analytic_distribution_id and ml.move_id.analytic_distribution_id.id or False, vals.get('account_id') or ml.account_id.id)
328
if distrib_state in ['invalid', 'none']:
329
vals.update({'state': 'draft'})
330
# Add account_id because of an error with account_activable module for checking date
331
if not 'account_id' in vals and 'date' in vals:
332
vals.update({'account_id': ml.account_id and ml.account_id.id or False})
333
check = self._check_employee_analytic_distribution(cr, uid, [ml.id], context={'from_write': True})
334
if check and isinstance(check, dict):
336
tmp_res = super(account_move_line, self).write(cr, uid, [ml.id], vals, context, False, False)
339
res = super(account_move_line, self).write(cr, uid, ids, vals, context, check, update_check)
342
def copy(self, cr, uid, id, default=None, context=None):
344
Copy analytic_distribution
352
res = super(account_move_line, self).copy(cr, uid, id, default, context)
353
# Update analytic distribution
355
c = self.browse(cr, uid, res, context=context)
356
if res and c.analytic_distribution_id:
357
new_distrib_id = self.pool.get('analytic.distribution').copy(cr, uid, c.analytic_distribution_id.id, {}, context=context)
359
self.write(cr, uid, [res], {'analytic_distribution_id': new_distrib_id}, context=context)
362
def get_analytic_move_lines(self, cr, uid, ids, context=None):
364
Return FP analytic lines attached to move lines
369
if 'active_ids' in context:
370
ids = context.get('active_ids')
371
if isinstance(ids, (int, long)):
374
domain = [('move_id', 'in', ids), ('account_id.category', '=', 'FUNDING')]
375
context.update({'display_fp': True})
377
'name': _('Analytic lines (FP) from Journal Items'),
378
'type': 'ir.actions.act_window',
379
'res_model': 'account.analytic.line',
381
'view_mode': 'tree,form',
387
def get_analytic_move_free1_lines(self, cr, uid, ids, context=None):
389
Return FREE1 analytic lines attached to move lines
394
if 'active_ids' in context:
395
ids = context.get('active_ids')
396
if isinstance(ids, (int, long)):
399
domain = [('move_id', 'in', ids), ('account_id.category', '=', 'FREE1')]
400
context.update({'display_fp': False, 'categ': 'FREE1'})
402
'name': _('Analytic Lines (Free 1) from Journal Items'),
403
'type': 'ir.actions.act_window',
404
'res_model': 'account.analytic.line',
406
'view_mode': 'tree,form',
412
def get_analytic_move_free2_lines(self, cr, uid, ids, context=None):
414
Return FREE2 analytic lines attached to move lines
419
if 'active_ids' in context:
420
ids = context.get('active_ids')
421
if isinstance(ids, (int, long)):
424
domain = [('move_id', 'in', ids), ('account_id.category', '=', 'FREE2')]
425
context.update({'display_fp': False, 'categ': 'FREE2'})
427
'name': _('Analytic Lines (Free 2) from Journal Items'),
428
'type': 'ir.actions.act_window',
429
'res_model': 'account.analytic.line',
431
'view_mode': 'tree,form',
438
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: