banking-addons-team team mailing list archive
-
banking-addons-team team
-
Mailing list archive
-
Message #02049
[Merge] lp:~therp-nl/banking-addons/7.0-camt_improvements into lp:banking-addons
Stefan Rijnhart (Therp) has proposed merging lp:~therp-nl/banking-addons/7.0-camt_improvements into lp:banking-addons.
Commit message:
[ADD] Allow import of zipfiles with multiple statements
[[IMP] CAMT: Append AddtlNtryInf to transaction message
[IMP] Further normalize CAMT identifiers against the account number
[RFR] Flake8
Requested reviews:
Banking Addons Core Editors (banking-addons-team)
For more details, see:
https://code.launchpad.net/~therp-nl/banking-addons/7.0-camt_improvements/+merge/224449
We encountered banks that don't seem to understand that a single CAMT file can encapsulate multiple bank statements from multiple banks, and instead offer a zip file with individual CAMT files per bank per day. This change allows any format of bank statements to be uploaded in zip format.
CAMT statements have meaningful identifiers, but they are very long. When importing bank statements, the identifiers are used as the basis of the bank statement name, which cannot be too long because this in turn is used as the basis of the move lines' references which are limited in length. After previous attempts of shortening the identifier, this change attempts to filter out the bank account from the identifier as is already represented in the rehashed identifier (in the form of its last n digits where n > 3 but long enough to be unique amongst the company bank accounts).
Legacy info is sometimes represented in a CAMT tag that was ignored previously. It is now simply added to the transaction's message.
As there are currently no other merge proposals on this part of the code, I took the opportunity to make the affected module parts pass flake8. This has lead to a very long diff, so please review the individual commits containing actual changes instead.
--
https://code.launchpad.net/~therp-nl/banking-addons/7.0-camt_improvements/+merge/224449
Your team Banking Addons Core Editors is requested to review the proposed merge of lp:~therp-nl/banking-addons/7.0-camt_improvements into lp:banking-addons.
=== modified file 'account_banking/__openerp__.py'
--- account_banking/__openerp__.py 2014-04-22 19:50:29 +0000
+++ account_banking/__openerp__.py 2014-06-25 13:41:22 +0000
@@ -26,7 +26,7 @@
{
'name': 'Account Banking',
- 'version': '0.4',
+ 'version': '0.5',
'license': 'AGPL-3',
'author': 'Banking addons community',
'website': 'https://launchpad.net/banking-addons',
=== modified file 'account_banking/parsers/models.py'
--- account_banking/parsers/models.py 2014-04-22 19:50:29 +0000
+++ account_banking/parsers/models.py 2014-06-25 13:41:22 +0000
@@ -20,6 +20,7 @@
##############################################################################
import re
+from difflib import SequenceMatcher
from openerp.tools.translate import _
class mem_bank_statement(object):
@@ -341,6 +342,24 @@
country_code = None
doc = __doc__
+ def normalize_identifier(self, account, identifier):
+ """
+ Strip any substantial part of the account number from
+ the identifier, as well as the common prefix 'CAMT053'.
+ """
+ if identifier.upper().startswith('CAMT053'):
+ identifier = identifier[7:]
+ seq_matcher = SequenceMatcher(None, account, identifier)
+ _a, start, length = seq_matcher.find_longest_match(
+ 0, len(account), 0, len(identifier))
+ if length < 7:
+ return identifier
+ result = identifier[0:start] + \
+ identifier[start + length:len(identifier)]
+ while result and not result[0].isalnum():
+ result = result[1:]
+ return result
+
def get_unique_statement_id(self, cr, base):
name = base
suffix = 1
=== modified file 'account_banking/wizard/__init__.py'
--- account_banking/wizard/__init__.py 2013-05-01 14:25:04 +0000
+++ account_banking/wizard/__init__.py 2014-06-25 13:41:22 +0000
@@ -2,11 +2,11 @@
##############################################################################
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
-# All Rights Reserved
+# (C) 2011-2014 Banking addons community
#
# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
+# it under the terms of the GNU Affero 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,
@@ -18,8 +18,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
-import bank_import
-import banking_transaction_wizard
-import link_partner
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+from . import bank_import
+from . import banking_transaction_wizard
+from . import link_partner
=== modified file 'account_banking/wizard/bank_import.py'
--- account_banking/wizard/bank_import.py 2013-10-31 07:33:46 +0000
+++ account_banking/wizard/bank_import.py 2014-06-25 13:41:22 +0000
@@ -2,11 +2,11 @@
##############################################################################
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
-# All Rights Reserved
+# (C) 2011 - 2014 Banking addons community
#
# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
+# it under the terms of the GNU Affero 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,
@@ -30,6 +30,8 @@
'''
import base64
import datetime
+from StringIO import StringIO
+from zipfile import ZipFile, BadZipfile # BadZipFile in Python >= 3.2
from openerp.osv import orm, fields
from openerp.tools.translate import _
from openerp.addons.account_banking.parsers import models
@@ -44,6 +46,7 @@
# the real payment date. This can occur with online transactions (web shops).
payment_window = datetime.timedelta(days=10)
+
def parser_types(*args, **kwargs):
'''Delay evaluation of parser types until start of wizard, to allow
depending modules to initialize and add their parsers to the list
@@ -57,17 +60,19 @@
_columns = {
'name': fields.char('Name', size=64),
'date': fields.date('Date', readonly=True),
- 'amount': fields.float('Amount', digits_compute=dp.get_precision('Account')),
+ 'amount': fields.float(
+ 'Amount', digits_compute=dp.get_precision('Account')),
'statement_line_id': fields.many2one(
'account.bank.statement.line',
'Resulting statement line', readonly=True),
'type': fields.selection([
- ('supplier','Supplier'),
- ('customer','Customer'),
- ('general','General')
+ ('supplier', 'Supplier'),
+ ('customer', 'Customer'),
+ ('general', 'General')
], 'Type', required=True),
'partner_id': fields.many2one('res.partner', 'Partner'),
- 'statement_id': fields.many2one('account.bank.statement', 'Statement',
+ 'statement_id': fields.many2one(
+ 'account.bank.statement', 'Statement',
select=True, required=True, ondelete='cascade'),
'ref': fields.char('Reference', size=32),
'note': fields.text('Notes'),
@@ -84,15 +89,15 @@
'line_id', 'invoice_id'),
'partner_bank_id': fields.many2one('res.partner.bank', 'Bank Account'),
'transaction_type': fields.selection([
- # TODO: payment terminal etc...
- ('invoice', 'Invoice payment'),
- ('storno', 'Canceled debit order'),
- ('bank_costs', 'Bank costs'),
- ('unknown', 'Unknown'),
- ], 'Transaction type'),
+ # TODO: payment terminal etc...
+ ('invoice', 'Invoice payment'),
+ ('storno', 'Canceled debit order'),
+ ('bank_costs', 'Bank costs'),
+ ('unknown', 'Unknown'),
+ ], 'Transaction type'),
'duplicate': fields.boolean('Duplicate'),
}
-
+
class banking_import(orm.TransientModel):
_name = 'account.banking.bank.import'
@@ -106,6 +111,14 @@
banking_import = self.browse(cr, uid, ids, context)[0]
statements_file = banking_import.file
data = base64.decodestring(statements_file)
+ files = [data]
+ try:
+ with ZipFile(StringIO(data), 'r') as archive:
+ files = [
+ archive.read(filename) for filename in archive.namelist()
+ ]
+ except BadZipfile:
+ pass
user_obj = self.pool.get('res.user')
statement_obj = self.pool.get('account.bank.statement')
@@ -119,30 +132,33 @@
if not parser:
raise orm.except_orm(
_('ERROR!'),
- _('Unable to import parser %(parser)s. Parser class not found.') %
- {'parser': parser_code}
+ _('Unable to import parser %(parser)s. '
+ 'Parser class not found.') % {'parser': parser_code}
)
# Get the company
company = (banking_import.company or
user_obj.browse(cr, uid, uid, context).company_id)
- # Parse the file
- statements = parser.parse(cr, data)
+ # Parse the file(s)
+ statements = []
+ for import_file in files:
+ statements += parser.parse(cr, import_file)
if any([x for x in statements if not x.is_valid()]):
raise orm.except_orm(
_('ERROR!'),
- _('The imported statements appear to be invalid! Check your file.')
+ _('The imported statements appear to be invalid! '
+ 'Check your file.')
)
# Create the file now, as the statements need to be linked to it
import_id = statement_file_obj.create(cr, uid, dict(
- company_id = company.id,
- file = statements_file,
- file_name = banking_import.file_name,
- state = 'unfinished',
- format = parser.name,
+ company_id=company.id,
+ file=statements_file,
+ file_name=banking_import.file_name,
+ state='unfinished',
+ format=parser.name,
))
bank_country_code = False
@@ -151,14 +167,14 @@
# Results
results = struct(
- stat_loaded_cnt = 0,
- trans_loaded_cnt = 0,
- stat_skipped_cnt = 0,
- trans_skipped_cnt = 0,
- trans_matched_cnt = 0,
- bank_costs_invoice_cnt = 0,
- error_cnt = 0,
- log = [],
+ stat_loaded_cnt=0,
+ trans_loaded_cnt=0,
+ stat_skipped_cnt=0,
+ trans_skipped_cnt=0,
+ trans_matched_cnt=0,
+ bank_costs_invoice_cnt=0,
+ error_cnt=0,
+ log=[],
)
# Caching
@@ -175,7 +191,8 @@
continue
# Create fallback currency code
- currency_code = statement.local_currency or company.currency_id.name
+ currency_code = statement.local_currency or \
+ company.currency_id.name
# Check cache for account info/currency
if statement.local_account in info and \
@@ -190,9 +207,8 @@
)
if not account_info:
results.log.append(
- _('Statements found for unknown account %(bank_account)s') %
- {'bank_account': statement.local_account}
- )
+ _('Statements found for unknown account %ss') %
+ statement.local_account)
error_accounts[statement.local_account] = True
results.error_cnt += 1
continue
@@ -200,8 +216,8 @@
results.log.append(
_('Statements found for account %(bank_account)s, '
'but no default journal was defined.'
- ) % {'bank_account': statement.local_account}
- )
+ ) % {'bank_account': statement.local_account}
+ )
error_accounts[statement.local_account] = True
results.error_cnt += 1
continue
@@ -222,22 +238,22 @@
and account_info.currency_id.name != statement.local_currency:
# TODO: convert currencies?
results.log.append(
- _('Statement %(statement_id)s for account %(bank_account)s'
+ _('Statement %(statement_id)s for account %(bank_account)s'
' uses different currency than the defined bank journal.'
- ) % {
- 'bank_account': statement.local_account,
- 'statement_id': statement.id
- }
+ ) % {
+ 'bank_account': statement.local_account,
+ 'statement_id': statement.id
+ }
)
error_accounts[statement.local_account] = True
results.error_cnt += 1
continue
# Check existence of previous statement
- # Less well defined formats can resort to a
+ # Less well defined formats can resort to a
# dynamically generated statement identification
# (e.g. a datetime string of the moment of import)
- # and have potential duplicates flagged by the
+ # and have potential duplicates flagged by the
# matching procedure
statement_ids = statement_obj.search(cr, uid, [
('name', '=', statement.id),
@@ -251,7 +267,8 @@
)
continue
- # Get the period for the statement (as bank statement object checks this)
+ # Get the period for the statement
+ # (as bank statement object checks this)
period_ids = period_obj.search(
cr, uid, [
('company_id', '=', company.id),
@@ -259,7 +276,7 @@
('date_stop', '>=', statement.date),
('special', '=', False),
], context=context)
-
+
if not period_ids:
results.log.append(
_('No period found covering statement date %(date)s, '
@@ -272,17 +289,17 @@
# Create the bank statement record
statement_id = statement_obj.create(cr, uid, dict(
- name = statement.id,
- journal_id = account_info.journal_id.id,
- date = convert.date2str(statement.date),
- balance_start = statement.start_balance,
- balance_end_real = statement.end_balance,
- balance_end = statement.end_balance,
- state = 'draft',
- user_id = uid,
- banking_id = import_id,
- company_id = company.id,
- period_id = period_ids[0],
+ name=statement.id,
+ journal_id=account_info.journal_id.id,
+ date=convert.date2str(statement.date),
+ balance_start=statement.start_balance,
+ balance_end_real=statement.end_balance,
+ balance_end=statement.end_balance,
+ state='draft',
+ user_id=uid,
+ banking_id=import_id,
+ company_id=company.id,
+ period_id=period_ids[0],
))
imported_statement_ids.append(statement_id)
@@ -294,7 +311,8 @@
values = {}
for attr in transaction.__slots__ + ['type']:
if attr in import_transaction_obj.column_map:
- values[import_transaction_obj.column_map[attr]] = eval('transaction.%s' % attr)
+ values[import_transaction_obj.column_map[attr]] = eval(
+ 'transaction.%s' % attr)
elif attr in import_transaction_obj._columns:
values[attr] = eval('transaction.%s' % attr)
values['statement_id'] = statement_id
@@ -305,35 +323,16 @@
transaction_id = import_transaction_obj.create(
cr, uid, values, context=context)
transaction_ids.append(transaction_id)
-
+
results.stat_loaded_cnt += 1
- import_transaction_obj.match(cr, uid, transaction_ids, results=results, context=context)
-
+ import_transaction_obj.match(
+ cr, uid, transaction_ids, results=results, context=context)
+
#recompute statement end_balance for validation
statement_obj.button_dummy(
cr, uid, imported_statement_ids, context=context)
-
- # Original code. Didn't take workflow logistics into account...
- #
- #cr.execute(
- # "UPDATE payment_order o "
- # "SET state = 'done', "
- # "date_done = '%s' "
- # "FROM payment_line l "
- # "WHERE o.state = 'sent' "
- # "AND o.id = l.order_id "
- # "AND l.id NOT IN ("
- # "SELECT DISTINCT id FROM payment_line "
- # "WHERE date_done IS NULL "
- # "AND id IN (%s)"
- # ")" % (
- # time.strftime('%Y-%m-%d'),
- # ','.join([str(x) for x in payment_line_ids])
- # )
- #)
-
report = [
'%s: %s' % (_('Total number of statements'),
results.stat_skipped_cnt + results.stat_loaded_cnt),
@@ -359,16 +358,16 @@
]
text_log = '\n'.join(report + results.log)
state = results.error_cnt and 'error' or 'ready'
- statement_file_obj.write(cr, uid, import_id, dict(
- state = state, log = text_log,
- ), context)
+ statement_file_obj.write(
+ cr, uid, import_id, dict(
+ state=state, log=text_log), context)
if not imported_statement_ids or not results.trans_loaded_cnt:
# file state can be 'ready' while import state is 'error'
state = 'error'
self.write(cr, uid, [ids[0]], dict(
- import_id = import_id, log = text_log, state = state,
- statement_ids = [(6, 0, imported_statement_ids)],
- ), context)
+ import_id=import_id, log=text_log, state=state,
+ statement_ids=[(6, 0, imported_statement_ids)],
+ ), context)
return {
'name': (state == 'ready' and _('Review Bank Statements') or
_('Error')),
@@ -394,12 +393,14 @@
'file_name': fields.char('File name', size=256),
'file': fields.binary(
'Statements File', required=True,
- help = ('The Transactions File to import. Please note that while it is '
- 'perfectly safe to reload the same file multiple times or to load in '
- 'timeframe overlapping statements files, there are formats that may '
- 'introduce different sequencing, which may create double entries.\n\n'
- 'To stay on the safe side, always load bank statements files using the '
- 'same format.'),
+ help=(
+ 'The Transactions File to import. Please note that while it '
+ 'is perfectly safe to reload the same file multiple times or '
+ 'to load in timeframe overlapping statements files, there '
+ 'are formats that may introduce different sequencing, which '
+ 'may create double entries.\n\nTo stay on the safe side, '
+ 'always load bank statements files using the '
+ 'same format.'),
states={
'ready': [('readonly', True)],
'error': [('readonly', True)],
@@ -435,8 +436,8 @@
_defaults = {
'state': 'init',
- 'company': lambda s,cr,uid,c:
- s.pool.get('res.company')._company_default_get(
+ 'company': lambda s, cr, uid, c:
+ s.pool.get('res.company')._company_default_get(
cr, uid, 'bank.import.transaction', context=c),
'parser': _default_parser_type,
}
=== modified file 'account_banking/wizard/banking_transaction_wizard.py'
--- account_banking/wizard/banking_transaction_wizard.py 2014-01-06 12:00:16 +0000
+++ account_banking/wizard/banking_transaction_wizard.py 2014-06-25 13:41:22 +0000
@@ -7,8 +7,8 @@
# All Rights Reserved
#
# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
+# it under the terms of the GNU Affero 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,
@@ -50,7 +50,7 @@
return res
def create_act_window(self, cr, uid, ids, nodestroy=True, context=None):
- """
+ """
Return a popup window for this model
"""
if isinstance(ids, (int, long)):
@@ -78,10 +78,10 @@
import_transaction_obj = self.pool.get('banking.import.transaction')
trans_id = self.read(
cr, uid, ids[0], ['import_transaction_id'],
- context=context)['import_transaction_id'][0] # many2one tuple
+ context=context)['import_transaction_id'][0] # many2one tuple
import_transaction_obj.match(cr, uid, [trans_id], context=context)
return self.create_act_window(cr, uid, ids, context=None)
-
+
def write(self, cr, uid, ids, vals, context=None):
"""
Implement a trigger to retrieve the corresponding move line
@@ -121,22 +121,24 @@
# Given the arity of the relation, there is are always
# multiple possibilities but the move lines here are
# prefiltered for having account_id.type payable/receivable
- # and the regular invoice workflow should only come up with
+ # and the regular invoice workflow should only come up with
# one of those only.
for move_line in wiz.import_transaction_id.move_line_ids:
if (move_line.invoice ==
wiz.import_transaction_id.invoice_id):
transaction_obj.write(
cr, uid, wiz.import_transaction_id.id,
- { 'move_line_id': move_line.id, }, context=context)
+ {'move_line_id': move_line.id},
+ context=context)
statement_line_obj.write(
- cr, uid, wiz.import_transaction_id.statement_line_id.id,
- { 'partner_id': move_line.partner_id.id or False,
- 'account_id': move_line.account_id.id,
- }, context=context)
+ cr, uid,
+ wiz.import_transaction_id.statement_line_id.id,
+ {'partner_id': move_line.partner_id.id,
+ 'account_id': move_line.account_id.id,
+ }, context=context)
found = True
break
- # Cannot match the invoice
+ # Cannot match the invoice
if not found:
orm.except_orm(
_("No entry found for the selected invoice"),
@@ -150,12 +152,14 @@
# Rewrite *2many directive notation
if manual_invoice_ids:
manual_invoice_ids = (
- [i[1] for i in manual_invoice_ids if i[0]==4] +
- [j for i in manual_invoice_ids if i[0]==6 for j in i[2]])
+ [i[1] for i in manual_invoice_ids if i[0] == 4] +
+ [j for i in manual_invoice_ids if i[0] == 6 for j in i[2]])
if manual_move_line_ids:
manual_move_line_ids = (
- [i[1] for i in manual_move_line_ids if i[0]==4] +
- [j for i in manual_move_line_ids if i[0]==6 for j in i[2]])
+ [i[1] for i in manual_move_line_ids if i[0] == 4] +
+ [j for i in manual_move_line_ids
+ if i[0] == 6 for j in i[2]
+ ])
for wiz in self.browse(cr, uid, ids, context=context):
#write can be called multiple times for the same values
#that doesn't hurt above, but it does here
@@ -171,7 +175,8 @@
found_move_line = False
if invoice.move_id:
for line in invoice.move_id.line_id:
- if line.account_id.type in ('receivable', 'payable'):
+ if line.account_id.type in (
+ 'receivable', 'payable'):
todo.append((invoice.id, line.id))
found_move_line = True
break
@@ -181,12 +186,8 @@
_("No entry found for the selected invoice. "))
for move_line_id in manual_move_line_ids:
todo_entry = [False, move_line_id]
- move_line=move_line_obj.read(
- cr,
- uid,
- move_line_id,
- ['invoice'],
- context=context)
+ move_line = move_line_obj.read(
+ cr, uid, move_line_id, ['invoice'], context=context)
if move_line['invoice']:
todo_entry[0] = move_line['invoice'][0]
todo.append(todo_entry)
@@ -194,25 +195,25 @@
while todo:
todo_entry = todo.pop()
move_line = move_line_obj.browse(
- cr, uid, todo_entry[1], context)
+ cr, uid, todo_entry[1], context)
transaction_id = wiz.import_transaction_id.id
statement_line_id = wiz.statement_line_id.id
if len(todo) > 0:
statement_line_id = wiz.statement_line_id.split_off(
- move_line.debit or -move_line.credit)[0]
+ move_line.debit or -move_line.credit)[0]
transaction_id = statement_line_obj.browse(
- cr,
- uid,
- statement_line_id,
- context=context).import_transaction_id.id
+ cr,
+ uid,
+ statement_line_id,
+ context=context).import_transaction_id.id
vals = {
'move_line_id': todo_entry[1],
'move_line_ids': [(6, 0, [todo_entry[1]])],
'invoice_id': todo_entry[0],
- 'invoice_ids': [(6, 0,
- [todo_entry[0]] if todo_entry[0] else [])],
+ 'invoice_ids': [
+ (6, 0, [todo_entry[0]] if todo_entry[0] else [])],
'match_type': 'manual',
}
@@ -221,7 +222,7 @@
st_line_vals = {
'account_id': move_line_obj.read(
- cr, uid, todo_entry[1],
+ cr, uid, todo_entry[1],
['account_id'], context=context)['account_id'][0],
}
@@ -231,7 +232,7 @@
).partner_id.commercial_partner_id.id
statement_line_obj.write(
- cr, uid, statement_line_id,
+ cr, uid, statement_line_id,
st_line_vals, context=context)
return res
@@ -255,22 +256,19 @@
# Get the bank account setting record, to reset the account
account_id = False
journal_id = wiz.statement_line_id.statement_id.journal_id.id
- setting_ids = settings_pool.find(cr, uid, journal_id, context=context)
+ setting_ids = settings_pool.find(
+ cr, uid, journal_id, context=context)
# Restore partner id from the bank account or else reset
partner_id = False
if (wiz.statement_line_id.partner_bank_id and
wiz.statement_line_id.partner_bank_id.partner_id):
- partner_id = wiz.statement_line_id.partner_bank_id.partner_id.id
+ partner_id = wiz.statement_line_id.partner_bank_id.\
+ partner_id.id
wiz.write({'partner_id': partner_id})
# Select account type by parter customer or supplier,
# or default based on amount sign
- if wiz.amount < 0:
- account_type = 'payable'
- else:
- account_type = 'receivable'
-
bank_partner = False
if partner_id:
bank_partner = wiz.statement_line_id.partner_bank_id.partner_id
@@ -297,11 +295,13 @@
if wiz.statement_line_id:
#delete splits causing an unsplit if this is a split
#transaction
- statement_pool.unlink(cr, uid,
- statement_pool.search(cr, uid,
- [('parent_id', '=', wiz.statement_line_id.id)],
- context=context),
- context=context)
+ statement_pool.unlink(
+ cr, uid,
+ statement_pool.search(
+ cr, uid,
+ [('parent_id', '=', wiz.statement_line_id.id)],
+ context=context),
+ context=context)
if wiz.import_transaction_id:
wiz.import_transaction_id.clear_and_write()
@@ -313,15 +313,15 @@
ids = [ids]
transaction_obj = self.pool.get('banking.import.transaction')
for wiz in self.read(
- cr, uid, ids, ['duplicate', 'import_transaction_id'],
- context=context):
+ cr, uid, ids, ['duplicate', 'import_transaction_id'],
+ context=context):
transaction_obj.write(
- cr, uid, wiz['import_transaction_id'][0],
+ cr, uid, wiz['import_transaction_id'][0],
{'duplicate': not wiz['duplicate']}, context=context)
return self.create_act_window(cr, uid, ids, context=None)
def button_done(self, cr, uid, ids, context=None):
- return {'type': 'ir.actions.act_window_close'}
+ return {'type': 'ir.actions.act_window_close'}
_columns = {
'name': fields.char('Name', size=64),
@@ -349,21 +349,23 @@
'statement_line_id', 'parent_id', type='many2one',
relation='account.bank.statement.line', readonly=True),
'import_transaction_id': fields.related(
- 'statement_line_id', 'import_transaction_id',
+ 'statement_line_id', 'import_transaction_id',
string="Import transaction",
type='many2one', relation='banking.import.transaction'),
'residual': fields.related(
- 'import_transaction_id', 'residual', type='float',
+ 'import_transaction_id', 'residual', type='float',
string='Residual', readonly=True),
'writeoff_account_id': fields.related(
'import_transaction_id', 'writeoff_account_id',
type='many2one', relation='account.account',
string='Write-off account'),
'invoice_ids': fields.related(
- 'import_transaction_id', 'invoice_ids', string="Matching invoices",
+ 'import_transaction_id', 'invoice_ids',
+ string="Matching invoices",
type='many2many', relation='account.invoice'),
'invoice_id': fields.related(
- 'import_transaction_id', 'invoice_id', string="Invoice to reconcile",
+ 'import_transaction_id', 'invoice_id',
+ string="Invoice to reconcile",
type='many2one', relation='account.invoice'),
'move_line_ids': fields.related(
'import_transaction_id', 'move_line_ids', string="Entry lines",
@@ -372,15 +374,16 @@
'import_transaction_id', 'move_line_id', string="Entry line",
type='many2one', relation='account.move.line'),
'duplicate': fields.related(
- 'import_transaction_id', 'duplicate', string='Flagged as duplicate',
+ 'import_transaction_id', 'duplicate',
+ string='Flagged as duplicate',
type='boolean'),
'match_multi': fields.related(
- 'import_transaction_id', 'match_multi',
+ 'import_transaction_id', 'match_multi',
type="boolean", string='Multiple matches'),
'match_type': fields.related(
'import_transaction_id', 'match_type', type='selection',
selection=[
- ('move','Move'),
+ ('move', 'Move'),
('invoice', 'Invoice'),
('payment', 'Payment line'),
('payment_order', 'Payment order'),
@@ -388,7 +391,7 @@
('manual', 'Manual'),
('payment_manual', 'Payment line (manual)'),
('payment_order_manual', 'Payment order (manual)'),
- ],
+ ],
string='Match type', readonly=True),
'manual_invoice_ids': fields.many2many(
'account.invoice',
@@ -401,8 +404,12 @@
'wizard_id', 'move_line_id', string='Or match one or more entries',
domain=[('account_id.reconcile', '=', True),
('reconcile_id', '=', False)]),
- 'payment_option': fields.related('import_transaction_id','payment_option', string='Payment Difference', type='selection', required=True,
- selection=[('without_writeoff', 'Keep Open'),('with_writeoff', 'Reconcile Payment Balance')]),
+ 'payment_option': fields.related(
+ 'import_transaction_id', 'payment_option',
+ string='Payment Difference',
+ type='selection', required=True,
+ selection=[('without_writeoff', 'Keep Open'),
+ ('with_writeoff', 'Reconcile Payment Balance')]),
'writeoff_analytic_id': fields.related(
'import_transaction_id', 'writeoff_analytic_id',
type='many2one', relation='account.analytic.account',
@@ -411,9 +418,7 @@
'statement_line_id', 'analytic_account_id',
type='many2one', relation='account.analytic.account',
string="Analytic Account"),
- 'move_currency_amount': fields.related('import_transaction_id','move_currency_amount',
+ 'move_currency_amount': fields.related(
+ 'import_transaction_id', 'move_currency_amount',
type='float', string='Match Currency Amount', readonly=True),
}
-
-banking_transaction_wizard()
-
=== modified file 'account_banking/wizard/banktools.py'
--- account_banking/wizard/banktools.py 2014-01-21 17:03:17 +0000
+++ account_banking/wizard/banktools.py 2014-06-25 13:41:22 +0000
@@ -2,11 +2,11 @@
##############################################################################
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
-# All Rights Reserved
+# (C) 2011-2014 Banking addons community
#
# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
+# it under the terms of the GNU Affero 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,
@@ -24,7 +24,7 @@
from openerp.addons.account_banking.struct import struct
__all__ = [
- 'get_period',
+ 'get_period',
'get_bank_accounts',
'get_partner',
'get_country_id',
@@ -32,6 +32,7 @@
'create_bank_account',
]
+
def get_period(pool, cr, uid, date, company, log=None):
'''
Wrapper over account_period.find() to log exceptions of
@@ -51,6 +52,7 @@
return False
return period_ids[0]
+
def get_bank_accounts(pool, cr, uid, account_number, log, fail=False):
'''
Get the bank account with account number account_number
@@ -72,6 +74,7 @@
return []
return partner_bank_obj.browse(cr, uid, bank_account_ids)
+
def _has_attr(obj, attr):
# Needed for dangling addresses and a weird exception scheme in
# OpenERP's orm.
@@ -80,6 +83,7 @@
except KeyError:
return False
+
def get_partner(pool, cr, uid, name, address, postal_code, city,
country_id, log, context=None):
'''
@@ -87,7 +91,7 @@
If multiple partners are found with the same name, select the first and
add a warning to the import log.
-
+
TODO: revive the search by lines from the address argument
'''
partner_obj = pool.get('res.partner')
@@ -115,7 +119,8 @@
key = name.lower()
partners = []
for partner in partner_obj.read(
- cr, uid, partner_search_ids, ['name', 'commercial_partner_id'], context=context):
+ cr, uid, partner_search_ids,
+ ['name', 'commercial_partner_id'], context=context):
if (len(partner['name']) > 3 and partner['name'].lower() in key):
partners.append(partner)
partners.sort(key=lambda x: len(x['name']), reverse=True)
@@ -126,6 +131,7 @@
'name %(name)s') % {'name': name})
return partner_ids and partner_ids[0] or False
+
def get_company_bank_account(pool, cr, uid, account_number, currency,
company, log):
'''
@@ -139,16 +145,16 @@
return False
elif len(bank_accounts) != 1:
log.append(
- _('More than one bank account was found with the same number %(account_no)s')
- % dict(account_no = account_number)
+ _('More than one bank account was found with the same number %s')
+ % account_number
)
return False
if bank_accounts[0].partner_id.id != company.partner_id.id:
log.append(
_('Account %(account_no)s is not owned by %(partner)s')
- % dict(account_no = account_number,
- partner = company.partner_id.name,
- ))
+ % dict(account_no=account_number,
+ partner=company.partner_id.name,
+ ))
return False
results.account = bank_accounts[0]
bank_settings_obj = pool.get('account.banking.account.settings')
@@ -189,8 +195,9 @@
return results
+
def get_or_create_bank(pool, cr, uid, bic, online=False, code=None,
- name=None):
+ name=None, context=None):
'''
Find or create the bank with the provided BIC code.
When online, the SWIFT database will be consulted in order to
@@ -231,38 +238,40 @@
bank_id = False
if online:
- info, address = bank_obj.online_bank_info(cr, uid, bic, context=context)
+ info, address = bank_obj.online_bank_info(
+ cr, uid, bic, context=context)
if info:
bank_id = bank_obj.create(cr, uid, dict(
- code = info.code,
- name = info.name,
- street = address.street,
- street2 = address.street2,
- zip = address.zip,
- city = address.city,
- country = country_id,
- bic = info.bic[:8],
+ code=info.code,
+ name=info.name,
+ street=address.street,
+ street2=address.street2,
+ zip=address.zip,
+ city=address.city,
+ country=country_id,
+ bic=info.bic[:8],
))
else:
info = struct(name=name, code=code)
if not online or not bank_id:
bank_id = bank_obj.create(cr, uid, dict(
- code = info.code or 'UNKNOW',
- name = info.name or _('Unknown Bank'),
- country = country_id,
- bic = bic,
+ code=info.code or 'UNKNOW',
+ name=info.name or _('Unknown Bank'),
+ country=country_id,
+ bic=bic,
))
return bank_id, country_id
+
def get_country_id(pool, cr, uid, transaction, context=None):
"""
Derive a country id from the info on the transaction.
-
+
:param transaction: browse record of a transaction
- :returns: res.country id or False
+ :returns: res.country id or False
"""
-
+
country_code = False
iban = sepa.IBAN(transaction.remote_account)
if iban.valid:
@@ -283,6 +292,7 @@
country_id = company.partner_id.country.id
return country_id
+
def create_bank_account(pool, cr, uid, partner_id,
account_number, holder_name, address, city,
country_id, bic=False,
@@ -291,9 +301,9 @@
Create a matching bank account with this holder for this partner.
'''
values = struct(
- partner_id = partner_id,
- owner_name = holder_name,
- country_id = country_id,
+ partner_id=partner_id,
+ owner_name=holder_name,
+ country_id=country_id,
)
# Are we dealing with IBAN?
@@ -325,5 +335,3 @@
# Create bank account and return
return pool.get('res.partner.bank').create(
cr, uid, values, context=context)
-
-# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
=== modified file 'account_banking/wizard/link_partner.py'
--- account_banking/wizard/link_partner.py 2014-01-21 07:55:55 +0000
+++ account_banking/wizard/link_partner.py 2014-06-25 13:41:22 +0000
@@ -24,6 +24,7 @@
from openerp.addons.account_banking.wizard import banktools
import ast
+
class link_partner(orm.TransientModel):
_name = 'banking.link_partner'
_description = 'Link partner'
@@ -66,14 +67,14 @@
'mobile': fields.char('Mobile', size=64),
'is_company': fields.boolean('Is a Company'),
}
-
+
_defaults = {
'is_company': True,
}
-
+
def create(self, cr, uid, vals, context=None):
"""
- Get default values from the transaction data
+ Get default values from the transaction data
on the statement line
"""
if vals and vals.get('statement_line_id'):
@@ -86,7 +87,7 @@
raise orm.except_orm(
_('Error'),
_('Statement line is already linked to a bank account '))
-
+
if not(transaction
and transaction.remote_account):
raise orm.except_orm(
@@ -124,17 +125,17 @@
return super(link_partner, self).create(
cr, uid, vals, context=context)
-
+
def update_partner_values(self, cr, uid, wizard, values, context=None):
"""
Updates the new partner values with the values from the wizard
-
+
:param wizard: read record of wizard (with load='_classic_write')
:param values: the dictionary of partner values that will be updated
"""
for field in ['is_company',
'name',
- 'street',
+ 'street',
'street2',
'zip',
'city',
@@ -148,7 +149,7 @@
if wizard[field]:
values[field] = wizard[field]
return True
-
+
def link_partner(self, cr, uid, ids, context=None):
statement_line_obj = self.pool.get(
'account.bank.statement.line')
@@ -159,14 +160,12 @@
else:
wiz_read = self.read(
cr, uid, ids[0], context=context, load='_classic_write')
- partner_vals = {
- 'type': 'default',
- }
+ partner_vals = {'type': 'default'}
self.update_partner_values(
cr, uid, wiz_read, partner_vals, context=context)
partner_id = self.pool.get('res.partner').create(
cr, uid, partner_vals, context=context)
-
+
partner_bank_id = banktools.create_bank_account(
self.pool, cr, uid, partner_id,
wiz.remote_account, wiz.name,
@@ -185,10 +184,10 @@
{'partner_bank_id': partner_bank_id,
'partner_id': partner_id}, context=context)
- return {'type': 'ir.actions.act_window_close'}
+ return {'type': 'ir.actions.act_window_close'}
def create_act_window(self, cr, uid, ids, nodestroy=True, context=None):
- """
+ """
Return a popup window for this model
"""
if isinstance(ids, (int, long)):
@@ -205,5 +204,3 @@
'res_id': ids[0],
'nodestroy': nodestroy,
}
-
-
=== modified file 'account_banking_camt/__init__.py'
--- account_banking_camt/__init__.py 2013-09-14 10:11:13 +0000
+++ account_banking_camt/__init__.py 2014-06-25 13:41:22 +0000
@@ -1,1 +1,1 @@
-import camt
+from . import camt
=== modified file 'account_banking_camt/__openerp__.py'
--- account_banking_camt/__openerp__.py 2013-09-18 15:27:30 +0000
+++ account_banking_camt/__openerp__.py 2014-06-25 13:41:22 +0000
@@ -1,11 +1,10 @@
##############################################################################
#
-# Copyright (C) 2013 Therp BV (<http://therp.nl>)
-# All Rights Reserved
+# Copyright (C) 2013-2014 Therp BV (<http://therp.nl>)
#
# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
+# it under the terms of the GNU Affero 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,
@@ -19,7 +18,7 @@
##############################################################################
{
'name': 'CAMT Format Bank Statements Import',
- 'version': '0.1',
+ 'version': '0.2',
'license': 'AGPL-3',
'author': 'Therp BV',
'website': 'https://launchpad.net/banking-addons',
=== modified file 'account_banking_camt/camt.py'
--- account_banking_camt/camt.py 2014-05-23 11:59:39 +0000
+++ account_banking_camt/camt.py 2014-06-25 13:41:22 +0000
@@ -1,12 +1,11 @@
# -*- coding: utf-8 -*-
##############################################################################
#
-# Copyright (C) 2013 Therp BV (<http://therp.nl>)
-# All Rights Reserved
+# Copyright (C) 2013-2014 Therp BV (<http://therp.nl>)
#
# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
+# it under the terms of the GNU Affero 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,
@@ -26,6 +25,7 @@
bt = models.mem_bank_transaction
+
class transaction(models.mem_bank_transaction):
def __init__(self, values, *args, **kwargs):
@@ -36,6 +36,7 @@
def is_valid(self):
return not self.error_message
+
class parser(models.parser):
code = 'CAMT'
country_code = 'NL'
@@ -69,7 +70,7 @@
def find(self, node, expr):
"""
Like xpath(), but return first result if any or else False
-
+
Return None to test nodes for being truesy
"""
result = node.xpath(expr, namespaces={'ns': self.ns[1:-1]})
@@ -82,19 +83,20 @@
:param node: BkToCstmrStmt/Stmt/Bal node
:param balance type: one of 'OPBD', 'PRCD', 'ITBD', 'CLBD'
"""
- code_expr = './ns:Bal/ns:Tp/ns:CdOrPrtry/ns:Cd[text()="%s"]/../../..' % balance_type
+ code_expr = ('./ns:Bal/ns:Tp/ns:CdOrPrtry/ns:Cd[text()="%s"]'
+ '/../../..') % balance_type
return self.xpath(node, code_expr)
-
+
def parse_amount(self, node):
"""
Parse an element that contains both Amount and CreditDebitIndicator
-
+
:return: signed amount
:returntype: float
"""
sign = -1 if node.find(self.ns + 'CdtDbtInd').text == 'DBIT' else 1
return sign * float(node.find(self.ns + 'Amt').text)
-
+
def get_start_balance(self, node):
"""
Find the (only) balance node with code OpeningBalance, or
@@ -137,9 +139,9 @@
if self.xpath(node, './ns:Acct/ns:Id/ns:IBAN')
else self.xpath(node, './ns:Acct/ns:Id/ns:Othr/ns:Id')[0].text)
- identifier = node.find(self.ns + 'Id').text
- if identifier.upper().startswith('CAMT053'):
- identifier = identifier[7:]
+ identifier = self.normalize_identifier(
+ statement.local_account,
+ node.find(self.ns + 'Id').text)
statement.id = self.get_unique_statement_id(
cr, "%s-%s" % (
self.get_unique_account_identifier(
@@ -191,6 +193,17 @@
vals = self.parse_TxDtls(TxDtls[0], entry_details)
else:
vals = entry_details
+ # Append additional entry info, which can contain remittance
+ # information in legacy format
+ Addtl = self.find(node, './ns:AddtlNtryInf')
+ if Addtl is not None and Addtl.text:
+ if vals.get('message'):
+ vals['message'] = '%s %s' % (vals['message'], Addtl.text)
+ else:
+ vals['message'] = Addtl.text
+ # Promote the message to reference if we don't have one yet
+ if not vals.get('reference') and vals.get('message'):
+ vals['reference'] = vals['message']
return vals
def get_party_values(self, TxDtls):
@@ -240,7 +253,7 @@
structured = self.find(
TxDtls, './ns:RmtInf/ns:Strd/ns:CdtrRefInf/ns:Ref')
if structured is None or not structured.text:
- structured = self.find(TxDtls, './ns:Refs/ns:EndToEndId')
+ structured = self.find(TxDtls, './ns:Refs/ns:EndToEndId')
if structured is not None:
vals['reference'] = structured.text
else:
Follow ups