@ -2,16 +2,19 @@
""" Framework for importing bank statement files. """
""" Framework for importing bank statement files. """
import logging
import logging
import base64
import base64
from StringIO import StringIO
from zipfile import ZipFile , BadZipfile # BadZipFile in Python >= 3.2
from openerp import api , models , fields
from openerp import api , models , fields
from openerp.tools.translate import _
from openerp.tools.translate import _
from openerp.exceptions import Warning
from openerp.exceptions import Warning as UserError
_logger = logging . getLogger ( __name__ )
_logger = logging . getLogger ( __name__ ) # pylint: disable=invalid-name
class AccountBankStatementLine ( models . Model ) :
class AccountBankStatementLine ( models . Model ) :
""" Extend model account.bank.statement.line. """
""" Extend model account.bank.statement.line. """
# pylint: disable=too-many-public-methods
_inherit = " account.bank.statement.line "
_inherit = " account.bank.statement.line "
# Ensure transactions can be imported only once (if the import format
# Ensure transactions can be imported only once (if the import format
@ -35,6 +38,7 @@ class AccountBankStatementImport(models.TransientModel):
""" Return False if the journal_id can ' t be provided by the parsed
""" Return False if the journal_id can ' t be provided by the parsed
file and must be provided by the wizard .
file and must be provided by the wizard .
See account_bank_statement_import_qif """
See account_bank_statement_import_qif """
# pylint: disable=no-self-use
return True
return True
journal_id = fields . Many2one (
journal_id = fields . Many2one (
@ -52,12 +56,14 @@ class AccountBankStatementImport(models.TransientModel):
@api.multi
@api.multi
def import_file ( self ) :
def import_file ( self ) :
""" Process the file chosen in the wizard, create bank statement(s) and
""" Process the file chosen in the wizard, create bank statement(s) and
go to reconciliation . """
go to reconciliation . """
self . ensure_one ( )
self . ensure_one ( )
data_file = base64 . b64decode ( self . data_file )
data_file = base64 . b64decode ( self . data_file )
# pylint: disable=protected-access
statement_ids , notifications = self . with_context (
statement_ids , notifications = self . with_context (
active_id = self . id ) . _import_file ( data_file )
active_id = self . id # pylint: disable=no-member
) . _import_file ( data_file )
# dispatch to reconciliation interface
# dispatch to reconciliation interface
action = self . env . ref (
action = self . env . ref (
' account.action_bank_reconcile_bank_statements ' )
' account.action_bank_reconcile_bank_statements ' )
@ -71,21 +77,47 @@ class AccountBankStatementImport(models.TransientModel):
' type ' : ' ir.actions.client ' ,
' type ' : ' ir.actions.client ' ,
}
}
@api.model
def _parse_all_files ( self , data_file ) :
""" Parse one file or multiple files from zip-file.
Return array of statements for further processing .
"""
statements = [ ]
files = [ data_file ]
try :
with ZipFile ( StringIO ( data_file ) , ' r ' ) as archive :
files = [
archive . read ( filename ) for filename in archive . namelist ( )
if not filename . endswith ( ' / ' )
]
except BadZipfile :
pass
# Parse the file(s)
for import_file in files :
# The appropriate implementation module(s) returns the statements.
# Actually we don't care wether all the files have the same
# format. Although unlikely you might mix mt940 and camt files
# in one zipfile.
parse_result = self . _parse_file ( import_file )
# Check for old version result, with separate currency and account
if isinstance ( parse_result , tuple ) and len ( parse_result ) == 3 :
( currency_code , account_number , new_statements ) = parse_result
for stmt_vals in new_statements :
stmt_vals [ ' currency_code ' ] = currency_code
stmt_vals [ ' account_number ' ] = account_number
else :
new_statements = parse_result
statements + = new_statements
return statements
@api.model
@api.model
def _import_file ( self , data_file ) :
def _import_file ( self , data_file ) :
""" Create bank statement(s) from file. """
""" Create bank statement(s) from file. """
# The appropriate implementation module returns the required data
# The appropriate implementation module returns the required data
statement_ids = [ ]
statement_ids = [ ]
notifications = [ ]
notifications = [ ]
parse_result = self . _parse_file ( data_file )
# Check for old version result, with separate currency and account
if isinstance ( parse_result , tuple ) and len ( parse_result ) == 3 :
( currency_code , account_number , statements ) = parse_result
for stmt_vals in statements :
stmt_vals [ ' currency_code ' ] = currency_code
stmt_vals [ ' account_number ' ] = account_number
else :
statements = parse_result
statements = self . _parse_all_files ( data_file )
# Check raw data:
# Check raw data:
self . _check_parsed_data ( statements )
self . _check_parsed_data ( statements )
# Import all statements:
# Import all statements:
@ -96,7 +128,7 @@ class AccountBankStatementImport(models.TransientModel):
statement_ids . append ( statement_id )
statement_ids . append ( statement_id )
notifications . extend ( new_notifications )
notifications . extend ( new_notifications )
if len ( statement_ids ) == 0 :
if len ( statement_ids ) == 0 :
raise Warning ( _ ( ' You have already imported that file. ' ) )
raise UserError ( _ ( ' You have already imported that file. ' ) )
return statement_ids , notifications
return statement_ids , notifications
@api.model
@api.model
@ -111,13 +143,14 @@ class AccountBankStatementImport(models.TransientModel):
currency_id = self . _find_currency_id ( currency_code )
currency_id = self . _find_currency_id ( currency_code )
bank_account_id = self . _find_bank_account_id ( account_number )
bank_account_id = self . _find_bank_account_id ( account_number )
if not bank_account_id and account_number :
if not bank_account_id and account_number :
raise Warning ( _ ( ' Can not find the account number %s . ' ) %
account_number )
raise UserError (
_ ( ' Can not find the account number %s . ' ) % account_number
)
# Find the bank journal
# Find the bank journal
journal_id = self . _get_journal ( currency_id , bank_account_id )
journal_id = self . _get_journal ( currency_id , bank_account_id )
# By now journal and account_number must be known
# By now journal and account_number must be known
if not journal_id :
if not journal_id :
raise Warning ( _ ( ' Can not determine journal for import. ' ) )
raise UserError ( _ ( ' Can not determine journal for import. ' ) )
# Prepare statement data to be used for bank statements creation
# Prepare statement data to be used for bank statements creation
stmt_vals = self . _complete_statement (
stmt_vals = self . _complete_statement (
stmt_vals , journal_id , account_number )
stmt_vals , journal_id , account_number )
@ -126,6 +159,8 @@ class AccountBankStatementImport(models.TransientModel):
@api.model
@api.model
def _parse_file ( self , data_file ) :
def _parse_file ( self , data_file ) :
# pylint: disable=no-self-use
# pylint: disable=unused-argument
""" Each module adding a file support must extends this method. It
""" Each module adding a file support must extends this method. It
processes the file if it can , returns super otherwise , resulting in a
processes the file if it can , returns super otherwise , resulting in a
chain of responsability .
chain of responsability .
@ -155,21 +190,22 @@ class AccountBankStatementImport(models.TransientModel):
- o ' partner_name ' : string
- o ' partner_name ' : string
- o ' ref ' : string
- o ' ref ' : string
"""
"""
raise Warning ( _ (
raise UserError ( _ (
' Could not make sense of the given file. \n '
' Could not make sense of the given file. \n '
' Did you install the module to support this type of file? '
' Did you install the module to support this type of file? '
) )
) )
@api.model
@api.model
def _check_parsed_data ( self , statements ) :
def _check_parsed_data ( self , statements ) :
# pylint: disable=no-self-use
""" Basic and structural verifications """
""" Basic and structural verifications """
if len ( statements ) == 0 :
if len ( statements ) == 0 :
raise Warning ( _ ( ' This file doesn \' t contain any statement. ' ) )
raise UserError ( _ ( ' This file doesn \' t contain any statement. ' ) )
for stmt_vals in statements :
for stmt_vals in statements :
if ' transactions ' in stmt_vals and stmt_vals [ ' transactions ' ] :
if ' transactions ' in stmt_vals and stmt_vals [ ' transactions ' ] :
return
return
# If we get here, no transaction was found:
# If we get here, no transaction was found:
raise Warning ( _ ( ' This file doesn \' t contain any transaction. ' ) )
raise UserError ( _ ( ' This file doesn \' t contain any transaction. ' ) )
@api.model
@api.model
def _find_currency_id ( self , currency_code ) :
def _find_currency_id ( self , currency_code ) :
@ -180,7 +216,7 @@ class AccountBankStatementImport(models.TransientModel):
if currency_ids :
if currency_ids :
return currency_ids [ 0 ] . id
return currency_ids [ 0 ] . id
else :
else :
raise Warning ( _ (
raise UserError ( _ (
' Statement has invalid currency code %s ' ) % currency_code )
' Statement has invalid currency code %s ' ) % currency_code )
# if no currency_code is provided, we'll use the company currency
# if no currency_code is provided, we'll use the company currency
return self . env . user . company_id . currency_id . id
return self . env . user . company_id . currency_id . id
@ -207,7 +243,7 @@ class AccountBankStatementImport(models.TransientModel):
if journal_id :
if journal_id :
if ( bank_account . journal_id . id and
if ( bank_account . journal_id . id and
bank_account . journal_id . id != journal_id ) :
bank_account . journal_id . id != journal_id ) :
raise Warning (
raise UserError (
_ ( ' The account of this statement is linked to '
_ ( ' The account of this statement is linked to '
' another journal. ' ) )
' another journal. ' ) )
if not bank_account . journal_id . id :
if not bank_account . journal_id . id :
@ -230,7 +266,7 @@ class AccountBankStatementImport(models.TransientModel):
currency_id ,
currency_id ,
journal_currency_id
journal_currency_id
)
)
raise Warning ( _ (
raise UserError ( _ (
' The currency of the bank statement is not '
' The currency of the bank statement is not '
' the same as the currency of the journal ! '
' the same as the currency of the journal ! '
) )
) )
@ -244,7 +280,7 @@ class AccountBankStatementImport(models.TransientModel):
currency_id ,
currency_id ,
company_currency_id
company_currency_id
)
)
raise Warning ( _ (
raise UserError ( _ (
' The currency of the bank statement is not '
' The currency of the bank statement is not '
' the same as the company currency ! '
' the same as the company currency ! '
) )
) )