~therp-nl/banking-addons/ba61-lp986088-fix_import_move_lines_without_invoice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# -*- encoding: utf-8 -*-
##############################################################################
#
#    Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>)
#                  2011 Therp BV (<http://therp.nl>)
#    All Rights Reserved
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################

'''
This parser follows the Dutch Banking Tools specifications which are
empirically recreated in this module.

Dutch Banking Tools uses the concept of 'Afschrift' or Bank Statement.
Every transaction is bound to a Bank Statement. As such, this module generates
Bank Statements along with Bank Transactions.
'''
from account_banking.parsers import models
from account_banking.parsers.convert import str2date
from account_banking.sepa import postalcode
from tools.translate import _
from osv import osv

import re
import csv

__all__ = ['parser']

bt = models.mem_bank_transaction

class transaction_message(object):
    '''
    A auxiliary class to validate and coerce read values
    '''
    attrnames = [
        'local_account', 'local_currency', 'date', 'u1', 'u2', 'date2',
        'transferred_amount', 'blob',
    ]

    def __init__(self, values, subno):
        '''
        Initialize own dict with attributes and coerce values to right type
        '''
        if len(self.attrnames) != len(values):
            raise ValueError, \
                    _('Invalid transaction line: expected %d columns, found '
                      '%d') % (len(self.attrnames), len(values))
        ''' Strip all values except the blob '''
        for (key, val) in zip(self.attrnames, values):
            self.__dict__[key] = key == 'blob' and val or val.strip()
        # for lack of a standardized locale function to parse amounts
        self.local_account = self.local_account.zfill(10)
        self.transferred_amount = float(
            self.transferred_amount.replace(',', '.'))
        self.execution_date = str2date(self.date, '%Y%m%d')
        self.effective_date = str2date(self.date, '%Y%m%d')
        # Set statement_id based on week number
        self.statement_id = self.effective_date.strftime('%Yw%W')
        self.id = str(subno).zfill(4)

class transaction(models.mem_bank_transaction):
    '''
    Implementation of transaction communication class for account_banking.
    '''
    attrnames = ['local_account', 'local_currency', 'transferred_amount',
                 'blob', 'execution_date', 'effective_date', 'id',
                ]

    type_map = {
        # retrieved from online help in the Triodos banking application
        'BEA': bt.PAYMENT_TERMINAL, # Pin
        'GEA': bt.BANK_TERMINAL, # ATM
        'COSTS': bt.BANK_COSTS,
        'BANK': bt.ORDER,
        'GIRO': bt.ORDER,
        'INTL': bt.ORDER, # international order
        'UNKN': bt.ORDER, # everything else
    }

    def __init__(self, line, *args, **kwargs):
        '''
        Initialize own dict with read values.
        '''
        super(transaction, self).__init__(*args, **kwargs)
        # Copy attributes from auxiliary class to self.
        for attr in self.attrnames:
            setattr(self, attr, getattr(line, attr))
        # Initialize other attributes
        self.transfer_type = 'UNKN'
        self.remote_account = ''
        self.remote_owner = ''
        self.reference = ''
        self.message = ''
        # Decompose structured messages
        self.parse_message()

    def is_valid(self):
        if not self.error_message:
            if not self.transferred_amount:
                self.error_message = "No transferred amount"
            elif not self.execution_date:
                self.error_message = "No execution date"
            elif not self.remote_account and self.transfer_type not in [
                'BEA', 'GEA', 'COSTS', 'UNKN',
                ]:
                self.error_message = _('No remote account for transaction type '
                                       '%s') % self.transfer_type
        if self.error_message:
            raise osv.except_osv(_('Error !'), _(self.error_message))
        return not self.error_message

    def parse_message(self):
        '''
        Parse structured message parts into appropriate attributes
        '''
        def split_blob(line):
            # here we split up the blob, which the last field in a tab
            # separated statement line the blob is a *space separated* fixed
            # field format with field length 32. Empty fields are ignored
            col = 0
            size = 33
            res = []
            while(len(line) > col * size):
                if line[col * size : (col + 1) * size - 1].strip():
                    res.append(line[col * size : (col + 1) * size - 1])
                col += 1
            return res

        def parse_type(field):
            # here we process the first field, which identifies the statement type
            # and in case of certain types contains additional information
            transfer_type = 'UNKN'
            remote_account = False
            remote_owner = False
            if field.startswith('GIRO '):
                transfer_type = 'GIRO'
                # columns 6 to 14 contain the left or right aligned account number
                remote_account = field[:15].strip().zfill(10)
                # column 15 contains a space
                # columns 16 to 31 contain remote owner
                remote_owner = field[16:32].strip() or False
            elif field.startswith('BEA '):
                transfer_type = 'BEA'
                # columns 6 to 16 contain the terminal identifier
                # column 17 contains a space
                # columns 18 to 31 contain date and time in DD.MM.YY/HH.MM format
            elif field.startswith('GEA '): 
                transfer_type = 'GEA'
                # columns 6 to 16 contain the terminal identifier
                # column 17 contains a space
                # columns 18 to 31 contain date and time in DD.MM.YY/HH.MM format
            elif field.startswith('MAANDBIJDRAGE ABNAMRO'):
                transfer_type = 'COSTS'
            elif re.match("^\s([0-9]+\.){3}[0-9]+\s", field):
                transfer_type = 'BANK'
                remote_account = field[1:13].strip().replace('.', '').zfill(10)
                # column 14 to 31 is either empty or contains the remote owner
                remote_owner = field[14:32].strip()
            elif re.match("^EL[0-9]{13}I", field):
                transfer_type = 'INTL'
            return (transfer_type, remote_account, remote_owner)
        
        fields = split_blob(self.blob)
        (self.transfer_type, self.remote_account, self.remote_owner) = parse_type(fields[0])

        # extract other information depending on type
        if self.transfer_type == 'GIRO':
            self.message = ' '.join(field.strip() for field in fields[1:])

        elif self.transfer_type == 'BEA':
            # second column contains remote owner and bank pass identification
            self.remote_owner = len(fields) > 1 and fields[1].split(',')[0].strip() or False
            # column 2 and up can contain additional messsages 
            # (such as transaction costs or currency conversion)
            self.message = ' '.join(field.strip() for field in fields)

        elif self.transfer_type == 'BANK':
            # second column contains the remote owner or the first message line
            if not self.remote_owner:
                self.remote_owner = len(fields) > 1 and fields[1].strip() or False
                self.message = ' '.join(field.strip() for field in fields[2:])
            else:
                self.message = ' '.join(field.strip() for field in fields[1:])

        elif self.transfer_type == 'INTL':
            # first column seems to consist of some kind of international transaction id
            self.reference = fields[0].strip()
            # second column seems to contain remote currency and amount
            # to be processed in a later release of this module
            self.message = len(fields) > 1 and fields[1].strip() or False
            # third column contains iban, preceeded by a slash forward
            if len(fields) > 2:
                if fields[2].startswith('/'):
                    self.remote_account = fields[2][1:].strip()
                else:
                    self.message += ' ' + fields[2].strip()
                # fourth column contains remote owner
                self.remote_owner = (len(fields) > 3 and fields[3].strip() or
                                     False)
                self.message += ' ' + (
                    ' '.join(field.strip() for field in fields[4:]))

        else:
            self.message = ' '.join(field.strip() for field in fields)

        if not self.reference:
            # the reference is sometimes flagged by the prefix "BETALINGSKENM."
            # but can be any numeric line really
            refexpr = re.compile("^\s*(BETALINGSKENM\.)?\s*([0-9]+ ?)+\s*$")
            for field in fields[1:]:
                m = refexpr.match(field)
                if m:
                    self.reference = m.group(2)
                    break

class statement(models.mem_bank_statement):
    '''
    Implementation of bank_statement communication class of account_banking
    '''
    def __init__(self, msg, *args, **kwargs):
        '''
        Set decent start values based on first transaction read
        '''
        super(statement, self).__init__(*args, **kwargs)
        self.id = msg.statement_id
        self.local_account = msg.local_account
        self.date = str2date(msg.date, '%Y%m%d')
        self.start_balance = self.end_balance = 0 # msg.start_balance
        self.import_transaction(msg)

    def import_transaction(self, msg):
        '''
        Import a transaction and keep some house holding in the mean time.
        '''
        trans = transaction(msg)
        self.end_balance += trans.transferred_amount
        self.transactions.append(trans)

class parser(models.parser):
    code = 'ABNAM'
    country_code = 'NL'
    name = _('Abnamro (NL)')
    doc = _('''\
The Dutch Abnamro format is a tab separated text format. The last of these
fields is itself a fixed length array containing transaction type, remote
account and owner. The bank does not provide a formal specification of the
format. Transactions are not explicitely tied to bank statements, although
each file covers a period of two weeks.
''')

    def parse(self, cr, data):
        result = []
        stmnt = None
        lines = data.split('\n')
        # Transaction lines are not numbered, so keep a tracer
        subno = 0
        statement_id = False
        for line in csv.reader(lines, delimiter = '\t', quoting=csv.QUOTE_NONE):
            # Skip empty (last) lines
            if not line:
                continue
            subno += 1
            msg = transaction_message(line, subno)
            if not statement_id:
                statement_id = self.get_unique_statement_id(
                    cr, msg.effective_date.strftime('%Yw%W'))
            msg.statement_id = statement_id
            if stmnt:
                stmnt.import_transaction(msg)
            else:
                stmnt = statement(msg)
        result.append(stmnt)
        return result

# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: