1
# -*- coding: utf-8 -*-
2
##############################################################################
4
# OpenERP - Account balance reporting engine
5
# Copyright (C) 2009 Pexego Sistemas Informáticos. All Rights Reserved
8
# This program is free software: you can redistribute it and/or modify
9
# it under the terms of the GNU 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.
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 General Public License for more details.
18
# You should have received a copy of the GNU General Public License
19
# along with this program. If not, see <http://www.gnu.org/licenses/>.
21
##############################################################################
23
Account balance report objects
25
Generic account balance report document (with header and detail lines).
26
Designed following the needs of the
27
Spanish/Spain localization.
29
from openerp.osv import orm,fields
30
from openerp.tools.translate import _
36
# CSS classes for the account line templates
37
CSS_CLASSES = [('default','Default'),('l1', 'Level 1'), ('l2', 'Level 2'),
38
('l3', 'Level 3'), ('l4', 'Level 4'), ('l5', 'Level 5')]
40
class account_balance_reporting(orm.Model):
42
Account balance report.
43
It stores the configuration/header fields of an account balance report,
44
and the linked lines of detail with the values of the accounting concepts
45
(values generated from the selected template lines of detail formulas).
47
_name = "account.balance.reporting"
50
'name': fields.char('Name', size=64, required=True, select=True),
51
'template_id': fields.many2one('account.balance.reporting.template',
52
'Template', ondelete='set null', required=True, select=True,
53
states={'calc_done': [('readonly', True)],
54
'done': [('readonly', True)]}),
55
'calc_date': fields.datetime("Calculation date", readonly=True),
56
'state': fields.selection([('draft','Draft'),
57
('calc','Processing'),
58
('calc_done','Processed'),
60
('canceled','Canceled')], 'State'),
61
'company_id': fields.many2one('res.company', 'Company',
62
ondelete='cascade', required=True, readonly=True,
63
states={'draft': [('readonly', False)]}),
64
'current_fiscalyear_id': fields.many2one('account.fiscalyear',
65
'Fiscal year 1', select=True, required=True,
66
states={'calc_done': [('readonly', True)],
67
'done': [('readonly', True)]}),
68
'current_period_ids': fields.many2many('account.period',
69
'account_balance_reporting_account_period_current_rel',
70
'account_balance_reporting_id', 'period_id',
71
'Fiscal year 1 periods',
72
states={'calc_done': [('readonly', True)],
73
'done': [('readonly', True)]}),
74
'previous_fiscalyear_id': fields.many2one('account.fiscalyear',
75
'Fiscal year 2', select=True,
76
states={'calc_done': [('readonly', True)],
77
'done': [('readonly', True)]}),
78
'previous_period_ids': fields.many2many('account.period',
79
'account_balance_reporting_account_period_previous_rel',
80
'account_balance_reporting_id', 'period_id',
81
'Fiscal year 2 periods',
82
states={'calc_done': [('readonly', True)],
83
'done': [('readonly', True)]}),
84
'line_ids': fields.one2many('account.balance.reporting.line',
86
states = {'done': [('readonly', True)]}),
90
'company_id': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context).company_id.id,
94
def action_calculate(self, cr, uid, ids, context=None):
95
"""Called when the user presses the Calculate button.
96
It will use the report template to generate lines of detail for the
97
report with calculated values."""
100
line_obj = self.pool.get('account.balance.reporting.line')
101
# Set the state to 'calculating'
102
self.write(cr, uid, ids, {
104
'calc_date': time.strftime('%Y-%m-%d %H:%M:%S')
106
for report in self.browse(cr, uid, ids, context=context):
107
# Clear the report data (unlink the lines of detail)
108
line_obj.unlink(cr, uid, [line.id for line in report.line_ids],
110
# Fill the report with a 'copy' of the lines of its template (if it has one)
111
if report.template_id:
112
for template_line in report.template_id.line_ids:
113
line_obj.create(cr, uid, {
114
'code': template_line.code,
115
'name': template_line.name,
116
'report_id': report.id,
117
'template_line_id': template_line.id,
119
'current_value': None,
120
'previous_value': None,
121
'sequence': template_line.sequence,
122
'css_class': template_line.css_class,
124
# Set the parents of the lines in the report
125
# Note: We reload the reports objects to refresh the lines of detail.
126
for report in self.browse(cr, uid, ids, context=context):
127
if report.template_id:
128
# Set line parents (now that they have been created)
129
for line in report.line_ids:
130
tmpl_line = line.template_line_id
131
if tmpl_line and tmpl_line.parent_id:
132
parent_line_ids = line_obj.search(cr, uid,
133
[('report_id', '=', report.id),
134
('code', '=', tmpl_line.parent_id.code)])
135
line_obj.write(cr, uid, line.id, {
136
'parent_id': (parent_line_ids and
137
parent_line_ids[0] or False),
139
# Calculate the values of the lines
140
# Note: We reload the reports objects to refresh the lines of detail.
141
for report in self.browse(cr, uid, ids, context=context):
142
if report.template_id:
143
# Refresh the report's lines values
144
for line in report.line_ids:
145
line.refresh_values()
146
# Set the report as calculated
147
self.write(cr, uid, [report.id], {
151
# Ouch! no template: Going back to draft state.
152
self.write(cr, uid, [report.id], {'state': 'draft'},
156
def action_confirm(self, cr, uid, ids, context=None):
157
"""Called when the user clicks the confirm button."""
158
self.write(cr, uid, ids, {'state': 'done'}, context=context)
161
def action_cancel(self, cr, uid, ids, context=None):
162
"""Called when the user clicks the cancel button."""
163
self.write(cr, uid, ids, {'state': 'canceled'}, context=context)
166
def action_recover(self, cr, uid, ids, context=None):
167
"""Called when the user clicks the draft button to create
168
a new workflow instance."""
169
self.write(cr, uid, ids, {'state': 'draft', 'calc_date': None},
171
wf_service = netsvc.LocalService("workflow")
173
wf_service.trg_create(uid, 'account.balance.reporting', id, cr)
176
def calculate_action(self, cr, uid, ids, context=None):
177
"""Calculate the selected balance report data."""
179
# Send the calculate signal to the balance report to trigger
181
wf_service = netsvc.LocalService('workflow')
182
wf_service.trg_validate(uid, 'account.balance.reporting', id,
187
class account_balance_reporting_line(orm.Model):
189
Account balance report line / Accounting concept
190
One line of detail of the balance report representing an accounting
191
concept with its values.
192
The accounting concepts follow a parent-children hierarchy.
193
Its values (current and previous) are calculated based on the 'value'
194
formula of the linked template line.
196
_name = "account.balance.reporting.line"
199
'report_id': fields.many2one('account.balance.reporting', 'Report',
201
'sequence': fields.char('Sequence', size=32, required=False),
202
'code': fields.char('Code', size=64, required=True, select=True),
203
'name': fields.char('Name', size=256, required=True, select=True),
204
'notes': fields.text('Notes'),
205
'current_value': fields.float('Fiscal year 1', digits=(16,2)),
206
'previous_value': fields.float('Fiscal year 2', digits=(16,2)),
207
'calc_date': fields.datetime("Calculation date"),
208
'css_class': fields.selection(CSS_CLASSES, 'CSS Class'),
209
'template_line_id': fields.many2one(
210
'account.balance.reporting.template.line',
211
'Line template', ondelete='set null'),
212
'parent_id': fields.many2one('account.balance.reporting.line',
213
'Parent', ondelete='cascade'),
214
'child_ids': fields.one2many('account.balance.reporting.line',
215
'parent_id', 'Children'),
219
'report_id': lambda self, cr, uid, context: context.get('report_id', None),
220
'css_class': 'default',
223
_order = "sequence, code"
226
('report_code_uniq', 'unique(report_id, code)',
227
_("The code must be unique for this report!"))
230
def name_get(self, cr, uid, ids, context=None):
231
"""Redefine the method to show the code in the name ("[code] name")."""
233
for item in self.browse(cr, uid, ids, context=context):
234
res.append((item.id, "[%s] %s" % (item.code, item.name)))
237
def name_search(self, cr, uid, name, args=[], operator='ilike',
238
context=None, limit=80):
239
"""Redefine the method to allow searching by code."""
242
ids = self.search(cr, uid, [('code','ilike',name)]+ args,
243
limit=limit, context=context)
245
ids = self.search(cr, uid, [('name',operator,name)]+ args,
246
limit=limit, context=context)
247
return self.name_get(cr, uid, ids, context=context)
249
def refresh_values(self, cr, uid, ids, context=None):
251
Recalculates the values of this report line using the
252
linked line report values formulas:
254
Depending on this formula the final value is calculated as follows:
255
- Empy report value: sum of (this concept) children values.
256
- Number with decimal point ("10.2"): that value (constant).
257
- Account numbers separated by commas ("430,431,(437)"): Sum of the account balances.
258
(The sign of the balance depends on the balance mode)
259
- Concept codes separated by "+" ("11000+12000"): Sum of those concepts values.
263
for line in self.browse(cr, uid, ids, context=context):
264
tmpl_line = line.template_line_id
265
balance_mode = int(tmpl_line.report_id.balance_mode)
268
report = line.report_id
269
# We use the same code to calculate both fiscal year values,
270
# just iterating over them.
271
for fyear in ('current', 'previous'):
273
if fyear == 'current':
274
tmpl_value = tmpl_line.current_value
275
elif fyear == 'previous':
276
tmpl_value = (tmpl_line.previous_value or
277
tmpl_line.current_value)
278
# Remove characters after a ";" (we use ; for comments)
280
tmpl_value = tmpl_value.split(';')[0]
281
if (fyear == 'current' and not report.current_fiscalyear_id) \
282
or (fyear == 'previous' and not report.previous_fiscalyear_id):
286
# Empy template value => sum of the children values
287
for child in line.child_ids:
288
if child.calc_date != child.report_id.calc_date:
289
# Tell the child to refresh its values
290
child.refresh_values()
291
# Reload the child data
292
child = self.browse(cr, uid, child.id,
294
if fyear == 'current':
295
value += child.current_value
296
elif fyear == 'previous':
297
value += child.previous_value
298
elif re.match(r'^\-?[0-9]*\.[0-9]*$', tmpl_value):
299
# Number with decimal points => that number value
301
value = float(tmpl_value)
302
elif re.match(r'^[0-9a-zA-Z,\(\)\*_\ ]*$', tmpl_value):
303
# Account numbers separated by commas => sum of the
304
# account balances. We will use the context to filter
305
# the accounts by fiscalyear and periods.
307
if fyear == 'current':
309
'fiscalyear': report.current_fiscalyear_id.id,
310
'periods': [p.id for p in report.current_period_ids],
312
elif fyear == 'previous':
314
'fiscalyear': report.previous_fiscalyear_id.id,
315
'periods': [p.id for p in report.previous_period_ids],
317
value = line._get_account_balance(tmpl_value,
319
elif re.match(r'^[\+\-0-9a-zA-Z_\*\ ]*$', tmpl_value):
320
# Account concept codes separated by "+" => sum of the
321
# concepts (template lines) values.
322
for line_code in re.findall(r'(-?\(?[0-9a-zA-Z_]*\)?)',
325
if line_code.startswith('-') or \
326
(line_code.startswith('(') and
327
balance_mode in (2, 4)):
329
line_code = line_code.strip('-()*')
330
# findall might return empty strings
332
# Search for the line (perfect match)
333
line_ids = self.search(cr, uid, [
334
('report_id','=', report.id),
335
('code', '=', line_code),
337
for child in self.browse(cr, uid, line_ids,
339
if child.calc_date != child.report_id.calc_date:
340
child.refresh_values()
341
# Reload the child data
342
child = self.browse(cr, uid, child.id,
344
if fyear == 'current':
345
value += child.current_value * sign
346
elif fyear == 'previous':
347
value += child.previous_value * sign
348
# Negate the value if needed
351
if fyear == 'current':
352
current_value = value
353
elif fyear == 'previous':
354
previous_value = value
356
self.write(cr, uid, line.id, {
357
'current_value': current_value,
358
'previous_value': previous_value,
359
'calc_date': line.report_id.calc_date,
363
def _get_account_balance(self, cr, uid, ids, code, balance_mode=0,
366
It returns the (debit, credit, balance*) tuple for a account with the
367
given code, or the sum of those values for a set of accounts
368
when the code is in the form "400,300,(323)"
370
Depending on the balance_mode, the balance is calculated as follows:
371
Mode 0: debit-credit for all accounts (default);
372
Mode 1: debit-credit, credit-debit for accounts in brackets;
373
Mode 2: credit-debit for all accounts;
374
Mode 3: credit-debit, debit-credit for accounts in brackets.
376
Also the user may specify to use only the debit or credit of the account
377
instead of the balance writing "debit(551)" or "credit(551)".
379
acc_obj = self.pool.get('account.account')
380
logger = logging.getLogger(__name__)
382
line = self.browse(cr, uid, ids[0], context=context)
383
company_id = line.report_id.company_id.id
384
# We iterate over the accounts listed in "code", so code can be
385
# a string like "430+431+432-438"; accounts split by "+" will be added,
386
# accounts split by "-" will be substracted.
387
for acc_code in re.findall('(-?\w*\(?[0-9a-zA-Z_]*\)?)', code):
388
# Check if the code is valid (findall might return empty strings)
389
acc_code = acc_code.strip()
391
# Check the sign of the code (substraction)
392
if acc_code.startswith('-'):
394
acc_code = acc_code[1:].strip() # Strip the sign
397
if re.match(r'^debit\(.*\)$', acc_code):
398
# Use debit instead of balance
400
acc_code = acc_code[6:-1] # Strip debit()
401
elif re.match(r'^credit\(.*\)$', acc_code):
402
# Use credit instead of balance
404
acc_code = acc_code[7:-1] # Strip credit()
407
# Calculate sign of the balance mode
409
if balance_mode in (1, 2, 3):
410
# for accounts in brackets or mode 2, the sign is reversed
411
if (acc_code.startswith('(') and acc_code.endswith(')')) \
412
or balance_mode == 2:
414
# Strip the brackets (if any)
415
if acc_code.startswith('(') and acc_code.endswith(')'):
416
acc_code = acc_code[1:-1]
417
# Search for the account (perfect match)
418
account_ids = acc_obj.search(cr, uid, [
419
('code', '=', acc_code),
420
('company_id','=', company_id)
423
# Search for a subaccount ending with '0'
424
account_ids = acc_obj.search(cr, uid, [
425
('code', '=like', '%s%%0' % acc_code),
426
('company_id','=', company_id)
429
logger.warning("Account with code '%s' not found!"
431
for account in acc_obj.browse(cr, uid, account_ids,
434
res -= account.debit * sign
435
elif mode == 'credit':
436
res += account.credit * sign
438
res += account.balance * sign * sign_mode