1
# -*- coding: utf-8 -*-
2
##############################################################################
4
# Copyright (c) Camptocamp SA - http://www.camptocamp.com
5
# Author: Arnaud WÃŒst ported by Nicolas Bessi
7
# This file is part of the c2c_budget module
9
# WARNING: This program as such is intended to be used by professional
10
# programmers who take the whole responsability of assessing all potential
11
# consequences resulting from its eventual inadequacies and bugs
12
# End users who are looking for a ready-to-use solution with commercial
13
# garantees and support are strongly adviced to contract a Free Software
16
# This program is Free Software; you can redistribute it and/or
17
# modify it under the terms of the GNU General Public License
18
# as published by the Free Software Foundation; either version 2
19
# of the License, or (at your option) any later version.
21
# This program is distributed in the hope that it will be useful,
22
# but WITHOUT ANY WARRANTY; without even the implied warranty of
23
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
# GNU General Public License for more details.
26
# You should have received a copy of the GNU General Public License
27
# along with this program; if not, write to the Free Software
28
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
29
from osv import fields, osv
32
from c2c_reporting_tools.c2c_helper import *
36
class c2c_budget_item(osv.osv):
37
""" camptocamp budget item. This is a link between
38
budgets and financial accounts. """
41
_name = "c2c_budget.item"
42
_description = "Budget items"
44
'code' : fields.char('Code', size=50, required=True),
45
'name' : fields.char('Name', size=200, required=True),
46
'active' : fields.boolean('Active'),
47
'parent_id' : fields.many2one('c2c_budget.item', 'Parent Item'),
48
'children_ids' : fields.one2many(
53
'account' : fields.many2many(
55
'c2c_budget_item_account_rel',
60
'note' : fields.text('Notes'),
61
'calculation' : fields.text('Calculation'),
62
'type' : fields.selection(
70
'sequence' : fields.integer('Sequence'),
71
'style' : fields.selection(
75
'invisible', 'Invisible')
83
'active' : lambda *a: True,
84
'type' : lambda *a: 'normal',
85
'style' : lambda *a: 'normal',
88
_order = 'sequence,name'
90
def get_real_values_from_analytic_accounts(self, cr, uid, item_id, periods,\
91
lines, company_id, currency_id, change_date, context=None):
92
"""return the sum of analytic move lines for
93
this item (and all subitems)"""
94
if context is None: context = {}
96
# filter the budget lines to work on
97
budget_line_obj = self.pool.get('c2c_budget.line')
98
budget_lines = budget_line_obj.filter_by_items(
106
# get the list of Analytic accounts related to those lines
107
aa_ids = budget_line_obj.get_analytic_accounts(
115
#get accounts (and subaccounts) related to the given item (and subitems)
116
general_accounts_ids = self.get_accounts(
128
if start_date is None or start_date > p.date_start:
129
start_date = p.date_start
130
if end_date is None or end_date < p.date_stop:
131
end_date = p.date_stop
134
#we have all informations to look for Analytic Accounts' lines
135
aa_lines_obj = self.pool.get('account.analytic.line')
136
aa_lines_ids = aa_lines_obj.search(
140
('date', '>=', start_date),
141
('date', '<=', end_date),
142
('general_account_id', 'in', general_accounts_ids),
143
('account_id', 'in', aa_ids)],
146
aa_lines = aa_lines_obj.browse(cr, uid, aa_lines_ids, context=context)
148
#now we have the lines, let's add them
151
result += c2c_helper.exchange_currency(
154
l.general_account_id.company_id.currency_id.id,
156
c2c_helper.parse_date(change_date)
164
def get_real_values(self, cr, uid, item, periods, company_id, \
165
currency_id, change_date, context=None):
166
"""return the sum of the account move lines for this item """
167
if context is None: context = {}
170
# get the list of accounts and subaccounts linked to this item
171
accounts = self.get_accounts(cr, uid, [item.id], company_id, context)
173
#get all move_lines linked to this item
174
move_line_obj = self.pool.get('account.move.line')
175
move_line_ids = move_line_obj.search(
179
('period_id', 'in', [p.id for p in periods]),
180
('account_id', 'in', accounts)
183
move_lines = move_line_obj.browse(cr, uid, move_line_ids, context=context)
189
if 'company_id' in move_line_obj._columns:
190
line_currency_id = l.company_id.currency_id.id
191
#get company's currency from account
193
line_currency_id = l.account_id.company_id.currency_id.id
205
result += sign * c2c_helper.exchange_currency(
210
c2c_helper.parse_date(change_date)
217
def get_sub_items(self, cr, item_ids):
218
""" return a flat list of ids of all items under
219
items in the tree structure """
220
parents_ids = item_ids
222
items_ids = copy(parents_ids)
226
#for each "level" of parent
227
while len(parents_ids) > 0:
228
#get all the sub items of this level
231
WHERE parent_id IN ( %s )
232
AND active """ % ','.join(map(str,parents_ids))
234
children_ids = map(lambda x: x[0], cr.fetchall())
235
items_ids += children_ids
237
#continue with next level
238
parents_ids = copy(children_ids)
240
#count the loops to avoid infinit loops
242
if (loop_counter > 100):
243
raise osv.except_osv(
245
"""It seems the item structure is recursive.
246
Please check and correct it before to run this action again"""
249
return c2c_helper.unique(items_ids)
253
def get_accounts(self, cr, uid, item_ids, company_id, context=None):
254
"""return a list of accounts ids and their sub accounts
255
linked to items (item_ids) and their subitems """
256
if context is None: context = {}
258
sub_items_ids = self.get_sub_items(cr, item_ids)
260
sub_items = self.browse(cr, uid, sub_items_ids)
261
#gather all account linked to all subitems
264
ids+= map (lambda x:x.id, i.account)
266
#get the list of sub accounts of gathered accounts
267
account_flat_list = self.pool.get('account.account').\
268
get_children_flat_list(
276
#here is the list of all accounts and subaccounts linked to items and subitems
277
return account_flat_list
281
def compute_view_items(self, items, items_values):
282
""" compute items (type "view") values that are based on calculations
283
return the items_values param where computed values
284
are added (or replaced)
285
items is a list of items objects
286
items_values is a dictionnary item_id => item_value
289
#prepare the dictionnary of values for formula remplacement.
290
# put in it normal items' values and view items' values that do not have formula
293
if (i.type == 'normal' or (i.type== 'view' and ((not i.calculation)\
294
or i.calculation.strip() == "")) ) \
295
and i.code and i.code.strip() != '':
296
value_dict[""+i.code] = items_values[i.id]
297
#this loop allow to use view items' results in formulas.
298
#count the number of errors that append. Loop until
299
#the number remain constant (=error in formulas)
300
#or reach 0 (=nothing more to compute)
301
previousErrorCounter = 0
305
#look throught the items if there is formula to compute?
307
#if this item is a view, we must maybe
308
#replace its value by a calculation (if not already done)
309
if i.type == 'view' \
311
and i.calculation.strip() != "" \
313
and (i.code+"" not in value_dict):
316
exec_env = {'result': 0}
317
#replace keys by values in formula
319
formula = i.calculation % value_dict
323
#try to run the formula
327
exec formula in exec_env
331
#retrive formula result
333
items_values[i.id] = exec_env['result']
334
value_dict[""+i.code] = exec_env['result']
336
items_values[i.id] = 'error'
338
#the number of errors in this loop equal to the previous loop.
339
#No chance to get better... let's exit the "while true" loop
340
if errorCounter == previousErrorCounter:
343
previousErrorCounter = errorCounter
349
def get_sorted_list(self, cr, uid, root_id):
350
"""return a list of items sorted by sequence to be used in reports
351
Data are returned in a list
352
(value=dictionnary(keys='id','code',
353
'name','level', sequence, type, style))
356
def bySequence(first, second):
357
"""callback function to sort """
358
if first['sequence'] > second['sequence']:
360
elif first['sequence'] < second['sequence']:
365
flat_tree = self.get_flat_tree(cr, uid, root_id)
366
flat_tree.sort(bySequence)
368
item_ids = map(lambda x:x['id'], flat_tree)
370
return self.browse(cr, uid, item_ids)
374
def get_flat_tree(self, cr, uid, root_id, level=0):
375
""" return informations about a buget items tree strcuture.
376
Data are returned in a list
377
(value=dictionnary(keys='id','code','name','level', sequence, type, style))
378
Data are sorted as in the pre-order walk
379
algorithm in order to allow to display easily the tree in rapports
387
Do not specify the level param when you call this method,
388
it is used for recursive calls
394
#this method is recursive so for the first call,
395
#we must init result with the root node
397
query = """SELECT id, code, name, sequence, type, style, %s as level
399
WHERE id = %s """ % (level, str(root_id))
402
result.append(cr.dictfetchall()[0])
407
query = """SELECT id, code, name, sequence, type, style, %s as level
411
ORDER BY sequence """ % (level, str(root_id))
413
query_result = cr.dictfetchall()
415
for child in query_result:
417
#recursive call to append the children right after the item
418
result += self.get_flat_tree(cr, uid, child['id'], level+1)
420
#check to avoid inifit loop
422
raise osv.except_osv(
424
'It seems the budget items structure \
425
is recursive (or too deep). \
426
Please check and correct it\
427
before to run this action again'
435
def _check_recursion(self, cr, uid, ids, context=None, parent=None):
436
""" use in _constraints[]: return false
437
if there is a recursion in the budget items structure """
439
#use the parent check_recursion function defined in orm.py
440
return super(c2c_budget_item,self)._check_recursion(
444
parent=parent or 'parent_id',
452
'Error ! You can not create recursive\
453
budget items structure.',
460
def name_search(self, cr, user, name, args=None,\
461
operator='ilike', context=None, limit=80):
462
"""search not only for a matching names but also
463
for a matching codes """
472
[('code',operator,name)]+ args,
479
[('name',operator,name)]+ args,
483
return self.name_get(
492
def search(self, cr, user, args, offset=0, \
493
limit=None, order=None, context=None, count=False):
494
""" special search. If we search an item from the budget
495
version form (in the budget lines)
496
then the choice is reduce to periods
497
that overlap the budget dates"""
501
parent_result = super(c2c_budget_item, self).search(cr, user, args, offset, limit, order, context, count)
503
if context != None and 'budget_id' in context \
504
and context['budget_id'] != False:
506
budget = self.pool.get('c2c_budget').browse(
509
context['budget_id'],
513
allowed_items = self.get_sub_items(
515
[budget.budget_item_id.id]
517
for i in parent_result:
518
if i in allowed_items:
522
result = parent_result
527
def unlink(self, cr, uid, ids, context={}):
528
""" delete subitems and catch the ugly error in case
529
of the item is in use in another object """
531
#build a list of all sub items
535
sub_ids = self.get_sub_items(cr, ids)
537
#delete item and all subitems
539
return super(c2c_budget_item, self).unlink(
546
raise osv.except_osv(
547
'Unable to delete the item',
548
'At least one of the items you are trying to\
549
delete or one of their subitems is still \
550
referenced by another element. \
551
Check there is no budget lines that link to \
552
these items and there is no budget that use \
553
these items as budget structure root.')