~pedro.baeza/openerp-spain/6.1-l10n_es_bank_statement-fy_fix

« back to all changes in this revision

Viewing changes to account_balance_reporting/account_balance_reporting.py

  • Committer: Pedro M. Baeza
  • Date: 2014-01-10 10:36:59 UTC
  • Revision ID: pedro.baeza@serviciosbaeza.com-20140110103659-g2iyd3c1gj3c3dg3
[ADD] account_balance_reporting: Motor de informes de cuentas anuales. Incluye las siguientes mejoras sobre la anterior versión:
- Añadida la opción de que si no se pone ninguna fórmula en el ejercicio fiscal 2, se utiliza la fórmula del ejercicio fiscal 1. De esta forma, se reduce la cantidad de datos (ya que normalmente la fórmula es la misma), se aligera la pantalla de datos, y se hace más intuitivo (teniendo que tocar sólo en un sitio en lugar de en dos cuando se modifica alguna fórmula).
- Añadida la posibilidad de parsear espacios, que hasta el momento no se admitían, lo cual provocaba también posibles fallos de cálculo que no eran reportados al usuario (no se mostraba ningún error, pero la línea se quedaba con valor 0), provocando confusión.
- Corregido cálculo del signo para cuentas credit y debit. Tal como estaba, la expresión -debit(xxx) o -credit(xxx) sumaba el debe o el haber, nunca restaba.
- Corregido cálculo de la expresión debit, que añadía la suma de los saldos deudores en positivo en lugar de en negativo, que es como saldría si se obtiene el saldo de una cuenta que sólo tiene entradas en el debe.
- Corregida utilización de paréntesis para códigos. En el caso del cálculo con códigos, siempre se invertía el signo, independientemente del modo de informe.
[IMP] l10n_es_account_balance_report:
- Se han actualizado las plantillas para tener sólo la fórmula del ejercicio fiscal 1, ya que en todos los casos la fórmula del ejercicio fiscal 2 era la misma.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
##############################################################################
 
3
#
 
4
#    OpenERP - Account balance reporting engine
 
5
#    Copyright (C) 2009 Pexego Sistemas Informáticos. All Rights Reserved
 
6
#    $Id$
 
7
#
 
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.
 
12
#
 
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.
 
17
#
 
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/>.
 
20
#
 
21
##############################################################################
 
22
"""
 
23
Account balance report objects
 
24
 
 
25
Generic account balance report document (with header and detail lines).
 
26
Designed following the needs of the
 
27
Spanish/Spain localization.
 
28
"""
 
29
from openerp.osv import orm,fields
 
30
from openerp.tools.translate import _
 
31
import re
 
32
import time
 
33
import netsvc
 
34
import logging
 
35
 
 
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')]
 
39
 
 
40
class account_balance_reporting(orm.Model):
 
41
    """
 
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).
 
46
    """
 
47
    _name = "account.balance.reporting"
 
48
 
 
49
    _columns = {
 
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'),
 
59
                                   ('done','Done'),
 
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',
 
85
                                    'report_id', 'Lines',
 
86
                                    states = {'done': [('readonly', True)]}),
 
87
    }
 
88
 
 
89
    _defaults = {
 
90
        'company_id': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context).company_id.id,
 
91
        'state': 'draft',
 
92
    }
 
93
 
 
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."""
 
98
        if context is None:
 
99
            context = {}
 
100
        line_obj = self.pool.get('account.balance.reporting.line')
 
101
        # Set the state to 'calculating'
 
102
        self.write(cr, uid, ids, {
 
103
            'state': 'calc',
 
104
            'calc_date': time.strftime('%Y-%m-%d %H:%M:%S')
 
105
        })
 
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],
 
109
                            context=context)
 
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,
 
118
                            'parent_id': None,
 
119
                            'current_value': None,
 
120
                            'previous_value': None,
 
121
                            'sequence': template_line.sequence,
 
122
                            'css_class': template_line.css_class,
 
123
                        }, context=context)
 
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),
 
138
                            }, context=context)
 
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], {
 
148
                    'state': 'calc_done'
 
149
                }, context=context)
 
150
            else:
 
151
                # Ouch! no template: Going back to draft state.
 
152
                self.write(cr, uid, [report.id], {'state': 'draft'},
 
153
                           context=context)
 
154
        return True
 
155
 
 
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)
 
159
        return True
 
160
 
 
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)
 
164
        return True
 
165
 
 
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},
 
170
                   context=context)
 
171
        wf_service = netsvc.LocalService("workflow")
 
172
        for id in ids:
 
173
            wf_service.trg_create(uid, 'account.balance.reporting', id, cr)
 
174
        return True
 
175
 
 
176
    def calculate_action(self, cr, uid, ids, context=None):
 
177
        """Calculate the selected balance report data."""
 
178
        for id in ids:
 
179
            # Send the calculate signal to the balance report to trigger
 
180
            # action_calculate.
 
181
            wf_service = netsvc.LocalService('workflow')
 
182
            wf_service.trg_validate(uid, 'account.balance.reporting', id,
 
183
                                    'calculate', cr)
 
184
        return 'close'
 
185
 
 
186
 
 
187
class account_balance_reporting_line(orm.Model):
 
188
    """
 
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.
 
195
    """
 
196
    _name = "account.balance.reporting.line"
 
197
 
 
198
    _columns = {
 
199
        'report_id': fields.many2one('account.balance.reporting', 'Report',
 
200
                                     ondelete='cascade'),
 
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'),
 
216
    }
 
217
 
 
218
    _defaults = {
 
219
        'report_id': lambda self, cr, uid, context: context.get('report_id', None),
 
220
        'css_class': 'default',
 
221
    }
 
222
 
 
223
    _order = "sequence, code"
 
224
 
 
225
    _sql_constraints = [
 
226
        ('report_code_uniq', 'unique(report_id, code)',
 
227
         _("The code must be unique for this report!"))
 
228
    ]
 
229
 
 
230
    def name_get(self, cr, uid, ids, context=None):
 
231
        """Redefine the method to show the code in the name ("[code] name")."""
 
232
        res = []
 
233
        for item in self.browse(cr, uid, ids, context=context):
 
234
            res.append((item.id, "[%s] %s" % (item.code, item.name)))
 
235
        return res
 
236
 
 
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."""
 
240
        ids = []
 
241
        if name:
 
242
            ids = self.search(cr, uid, [('code','ilike',name)]+ args,
 
243
                              limit=limit, context=context)
 
244
        if not ids:
 
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)
 
248
 
 
249
    def refresh_values(self, cr, uid, ids, context=None):
 
250
        """
 
251
        Recalculates the values of this report line using the
 
252
        linked line report values formulas:
 
253
 
 
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.
 
260
        """
 
261
        if context is None:
 
262
            context = {}
 
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)
 
266
            current_value = 0.0
 
267
            previous_value = 0.0
 
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'):
 
272
                value = 0
 
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)
 
279
                if tmpl_value:
 
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):
 
283
                    value = 0
 
284
                else:
 
285
                    if not tmpl_value:
 
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,
 
293
                                                    context=context)
 
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
 
300
                        # (constant).
 
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.
 
306
                        ctx = context.copy()
 
307
                        if fyear == 'current':
 
308
                            ctx.update({
 
309
                                'fiscalyear': report.current_fiscalyear_id.id,
 
310
                                'periods': [p.id for p in report.current_period_ids],
 
311
                            })
 
312
                        elif fyear == 'previous':
 
313
                            ctx.update({
 
314
                                'fiscalyear': report.previous_fiscalyear_id.id,
 
315
                                'periods': [p.id for p in report.previous_period_ids],
 
316
                            })
 
317
                        value = line._get_account_balance(tmpl_value,
 
318
                                                          balance_mode, ctx)
 
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_]*\)?)',
 
323
                                                    tmpl_value):
 
324
                            sign = 1
 
325
                            if line_code.startswith('-') or \
 
326
                                    (line_code.startswith('(') and 
 
327
                                     balance_mode in (2, 4)):
 
328
                                sign = -1
 
329
                            line_code = line_code.strip('-()*')
 
330
                            # findall might return empty strings
 
331
                            if line_code:
 
332
                                # Search for the line (perfect match)
 
333
                                line_ids = self.search(cr, uid, [
 
334
                                        ('report_id','=', report.id),
 
335
                                        ('code', '=', line_code),
 
336
                                    ], context=context)
 
337
                                for child in self.browse(cr, uid, line_ids,
 
338
                                                         context=context):
 
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,
 
343
                                                            context=context)
 
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
 
349
                if tmpl_line.negate:
 
350
                    value = -value
 
351
                if fyear == 'current':
 
352
                    current_value = value
 
353
                elif fyear == 'previous':
 
354
                    previous_value = value
 
355
            # Write the values
 
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,
 
360
                }, context=context)
 
361
        return True
 
362
 
 
363
    def _get_account_balance(self, cr, uid, ids, code, balance_mode=0,
 
364
                             context=None):
 
365
        """
 
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)"
 
369
 
 
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.
 
375
 
 
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)".
 
378
        """
 
379
        acc_obj = self.pool.get('account.account')
 
380
        logger = logging.getLogger(__name__)
 
381
        res = 0.0
 
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()
 
390
            if acc_code:
 
391
                # Check the sign of the code (substraction)
 
392
                if acc_code.startswith('-'):
 
393
                    sign = -1
 
394
                    acc_code = acc_code[1:].strip() # Strip the sign
 
395
                else:
 
396
                    sign = 1
 
397
                if re.match(r'^debit\(.*\)$', acc_code):
 
398
                    # Use debit instead of balance
 
399
                    mode = 'debit'
 
400
                    acc_code = acc_code[6:-1] # Strip debit()
 
401
                elif re.match(r'^credit\(.*\)$', acc_code):
 
402
                    # Use credit instead of balance
 
403
                    mode = 'credit'
 
404
                    acc_code = acc_code[7:-1] # Strip credit()
 
405
                else:
 
406
                    mode = 'balance'
 
407
                # Calculate sign of the balance mode
 
408
                sign_mode = 1
 
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:
 
413
                        sign_mode = -1
 
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)
 
421
                    ], context=context)
 
422
                if not account_ids:
 
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)
 
427
                        ], context=context)
 
428
                if not account_ids:
 
429
                    logger.warning("Account with code '%s' not found!"
 
430
                                   %acc_code)
 
431
                for account in acc_obj.browse(cr, uid, account_ids,
 
432
                                              context=context):
 
433
                    if mode == 'debit':
 
434
                        res -= account.debit * sign
 
435
                    elif mode == 'credit':
 
436
                        res += account.credit * sign
 
437
                    else:
 
438
                        res += account.balance * sign * sign_mode
 
439
        return res