[MIG] account_statement_import_ofx to v14

Rename module to account_statement_import_ofx
Add support for multi-account OFX files
This commit is contained in:
Alexis de Lattre
2020-11-18 00:37:47 +01:00
parent 7ad314d53e
commit cb2a897b0e
32 changed files with 127 additions and 134 deletions

View File

@@ -1 +0,0 @@
from . import account_bank_statement_import

View File

@@ -1,100 +0,0 @@
import io
import logging
from odoo import _, api, models
from odoo.exceptions import UserError
from odoo.addons.base_iban.models.res_partner_bank import (
_map_iban_template,
validate_iban,
)
_logger = logging.getLogger(__name__)
try:
from ofxparse import OfxParser
except ImportError:
_logger.debug("ofxparse not found.")
OfxParser = None
class AccountBankStatementImport(models.TransientModel):
_inherit = "account.bank.statement.import"
def _check_journal_bank_account(self, journal, account_number):
res = super()._check_journal_bank_account(journal, account_number)
if not res:
e_acc_num = journal.bank_account_id.sanitized_acc_number
e_acc_num = e_acc_num.replace(" ", "")
validate_iban(e_acc_num)
country_code = e_acc_num[:2].lower()
iban_template = _map_iban_template[country_code].replace(" ", "")
e_acc_num = "".join(
[c for c, t in zip(e_acc_num, iban_template) if t == "C"]
)
res = e_acc_num == account_number
return res
@api.model
def _check_ofx(self, data_file):
if not OfxParser:
return False
try:
ofx = OfxParser.parse(io.BytesIO(data_file))
except Exception as e:
_logger.debug(e)
return False
return ofx
@api.model
def _prepare_ofx_transaction_line(self, transaction):
# Since ofxparse doesn't provide account numbers,
# we cannot provide the key 'bank_account_id',
# nor the key 'account_number'
# If you read odoo10/addons/account_bank_statement_import/
# account_bank_statement_import.py, it's the only 2 keys
# we can provide to match a partner.
name = transaction.payee
if transaction.checknum:
name += " " + transaction.checknum
if transaction.memo:
name += " : " + transaction.memo
vals = {
"date": transaction.date,
"name": name,
"ref": transaction.id,
"amount": float(transaction.amount),
"unique_import_id": transaction.id,
}
return vals
def _parse_file(self, data_file):
ofx = self._check_ofx(data_file)
if not ofx:
return super()._parse_file(data_file)
transactions = []
total_amt = 0.00
try:
for transaction in ofx.account.statement.transactions:
vals = self._prepare_ofx_transaction_line(transaction)
if vals:
transactions.append(vals)
total_amt += vals["amount"]
except Exception as e:
raise UserError(
_(
"The following problem occurred during import. "
"The file might not be valid.\n\n %s"
)
% e.message
)
balance = float(ofx.account.statement.balance)
vals_bank_statement = {
"name": ofx.account.number,
"transactions": transactions,
"balance_start": balance - total_amt,
"balance_end_real": balance,
}
return ofx.account.statement.currency, ofx.account.number, [vals_bank_statement]

View File

@@ -1,7 +1,7 @@
{ {
"name": "Import OFX Bank Statement", "name": "Import OFX Bank Statement",
"category": "Banking addons", "category": "Banking addons",
"version": "13.0.1.0.0", "version": "14.0.1.0.0",
"license": "AGPL-3", "license": "AGPL-3",
"author": "Odoo SA," "author": "Odoo SA,"
"Akretion," "Akretion,"
@@ -11,8 +11,8 @@
"Le Filament," "Le Filament,"
"Odoo Community Association (OCA)", "Odoo Community Association (OCA)",
"website": "https://github.com/OCA/bank-statement-import", "website": "https://github.com/OCA/bank-statement-import",
"depends": ["account_bank_statement_import",], "depends": ["account_statement_import"],
"data": ["views/view_account_bank_statement_import.xml",], "data": ["views/account_statement_import.xml"],
"external_dependencies": {"python": ["ofxparse"],}, "external_dependencies": {"python": ["ofxparse"]},
"installable": True, "installable": True,
} }

View File

@@ -5,8 +5,7 @@ class AccountJournal(models.Model):
_inherit = "account.journal" _inherit = "account.journal"
def _get_bank_statements_available_import_formats(self): def _get_bank_statements_available_import_formats(self):
""" Adds ofx to supported import formats. """Adds ofx to supported import formats."""
"""
rslt = super()._get_bank_statements_available_import_formats() rslt = super()._get_bank_statements_available_import_formats()
rslt.append("ofx") rslt.append("ofx")
return rslt return rslt

View File

@@ -2,3 +2,5 @@ This module adds support for the import of bank statements in `OFX format <https
Bank Statements may be generated containing a subset of the OFX information (only those transaction lines that are required for the Bank Statements may be generated containing a subset of the OFX information (only those transaction lines that are required for the
creation of the Financial Accounting records). creation of the Financial Accounting records).
Since v14, this module support multi-account OFX files i.e. several different bank accounts in the same OFX file.

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -12,13 +12,12 @@ class TestOfxFile(TransactionCase):
def setUp(self): def setUp(self):
super(TestOfxFile, self).setUp() super(TestOfxFile, self).setUp()
self.absi_model = self.env["account.bank.statement.import"] self.asi_model = self.env["account.statement.import"]
self.abs_model = self.env["account.bank.statement"] self.abs_model = self.env["account.bank.statement"]
self.j_model = self.env["account.journal"] self.j_model = self.env["account.journal"]
self.absl_model = self.env["account.bank.statement.line"] self.absl_model = self.env["account.bank.statement.line"]
self.ia_model = self.env["ir.attachment"]
cur = self.env.ref("base.USD") cur = self.env.ref("base.USD")
self.env.ref("base.main_company").currency_id = cur.id # self.env.ref("base.main_company").currency_id = cur.id
bank = self.env["res.partner.bank"].create( bank = self.env["res.partner.bank"].create(
{ {
"acc_number": "123456", "acc_number": "123456",
@@ -33,6 +32,7 @@ class TestOfxFile(TransactionCase):
"code": "BNK12", "code": "BNK12",
"type": "bank", "type": "bank",
"bank_account_id": bank.id, "bank_account_id": bank.id,
"currency_id": cur.id,
} }
) )
@@ -51,56 +51,60 @@ class TestOfxFile(TransactionCase):
"code": "BNK13", "code": "BNK13",
"type": "bank", "type": "bank",
"bank_account_id": bank_iban_ofx.id, "bank_account_id": bank_iban_ofx.id,
"currency_id": cur.id,
} }
) )
def test_wrong_ofx_file_import(self): def test_wrong_ofx_file_import(self):
ofx_file_path = get_module_resource( ofx_file_path = get_module_resource(
"account_bank_statement_import_ofx", "account_statement_import_ofx",
"tests/test_ofx_file/", "tests/test_ofx_file/",
"test_ofx_wrong.ofx", "test_ofx_wrong.ofx",
) )
ofx_file_wrong = base64.b64encode(open(ofx_file_path, "rb").read()) ofx_file_wrong = base64.b64encode(open(ofx_file_path, "rb").read())
attach = self.ia_model.create( bank_statement = self.asi_model.create(
{"name": "test_ofx_wrong.ofx", "datas": ofx_file_wrong,} {
) "statement_file": ofx_file_wrong,
bank_statement = self.absi_model.create( "statement_filename": "test_ofx_wrong.ofx",
dict(attachment_ids=[(6, 0, [attach.id])]) }
) )
self.assertFalse(bank_statement._check_ofx(data_file=ofx_file_wrong)) self.assertFalse(bank_statement._check_ofx(data_file=ofx_file_wrong))
def test_ofx_file_import(self): def test_ofx_file_import(self):
ofx_file_path = get_module_resource( ofx_file_path = get_module_resource(
"account_bank_statement_import_ofx", "tests/test_ofx_file/", "test_ofx.ofx" "account_statement_import_ofx", "tests/test_ofx_file/", "test_ofx.ofx"
) )
ofx_file = base64.b64encode(open(ofx_file_path, "rb").read()) ofx_file = base64.b64encode(open(ofx_file_path, "rb").read())
attach = self.ia_model.create({"name": "test_ofx.ofx", "datas": ofx_file,}) bank_statement = self.asi_model.create(
bank_statement = self.absi_model.create( {
dict(attachment_ids=[(6, 0, [attach.id])]) "statement_file": ofx_file,
"statement_filename": "test_ofx.ofx",
}
) )
bank_statement.import_file() bank_statement.import_file_button()
bank_st_record = self.abs_model.search([("name", "like", "123456")])[0] bank_st_record = self.abs_model.search([("name", "like", "123456")])[0]
self.assertEqual(bank_st_record.balance_start, 2516.56) self.assertEqual(bank_st_record.balance_start, 2516.56)
self.assertEqual(bank_st_record.balance_end_real, 2156.56) self.assertEqual(bank_st_record.balance_end_real, 2156.56)
line = self.absl_model.search( line = self.absl_model.search(
[("name", "=", "Agrolait"), ("statement_id", "=", bank_st_record.id)] [
("payment_ref", "=", "Agrolait"),
("statement_id", "=", bank_st_record.id),
]
)[0] )[0]
self.assertEqual(line.ref, "219378")
self.assertEqual(line.date, datetime.date(2013, 8, 24)) self.assertEqual(line.date, datetime.date(2013, 8, 24))
def test_check_journal_bank_account(self): def test_check_journal_bank_account(self):
ofx_file_path = get_module_resource( ofx_file_path = get_module_resource(
"account_bank_statement_import_ofx", "account_statement_import_ofx",
"tests/test_ofx_file/", "tests/test_ofx_file/",
"test_ofx_iban.ofx", "test_ofx_iban.ofx",
) )
ofx_file = base64.b64encode(open(ofx_file_path, "rb").read()) ofx_file = base64.b64encode(open(ofx_file_path, "rb").read())
attach = self.ia_model.create({"name": "test_ofx.ofx", "datas": ofx_file,}) bank_st = self.asi_model.create(
bank_st = self.absi_model.create(dict(attachment_ids=[(6, 0, [attach.id])])) {
journal_iban_ofx = self.j_model.search( "statement_file": ofx_file,
[("name", "=", "FR7630001007941234567890185")] "statement_filename": "test_ofx_iban.ofx",
}
) )
res = bank_st._check_journal_bank_account(journal_iban_ofx, "12345678901") bank_st.import_file_button()
self.assertTrue(res)
bank_st.with_context(journal_id=journal_iban_ofx.id).import_file()

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8" ?> <?xml version="1.0" encoding="utf-8" ?>
<odoo> <odoo>
<record id="view_account_bank_statement_import_form" model="ir.ui.view"> <record id="account_statement_import_form" model="ir.ui.view">
<field name="model">account.bank.statement.import</field> <field name="model">account.statement.import</field>
<field <field
name="inherit_id" name="inherit_id"
ref="account_bank_statement_import.account_bank_statement_import_view" ref="account_statement_import.account_statement_import_form"
/> />
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//ul[@id='statement_format']" position="inside"> <xpath expr="//ul[@id='statement_format']" position="inside">

View File

@@ -0,0 +1 @@
from . import account_statement_import

View File

@@ -0,0 +1,88 @@
import io
import logging
from odoo import _, api, models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
try:
from ofxparse import OfxParser
except ImportError:
_logger.debug("ofxparse not found.")
OfxParser = None
class AccountStatementImport(models.TransientModel):
_inherit = "account.statement.import"
@api.model
def _check_ofx(self, data_file):
if not OfxParser:
return False
try:
ofx = OfxParser.parse(io.BytesIO(data_file))
except Exception as e:
_logger.debug(e)
return False
return ofx
@api.model
def _prepare_ofx_transaction_line(self, transaction):
# Since ofxparse doesn't provide account numbers,
# we cannot provide the key 'bank_account_id',
# nor the key 'account_number'
# If you read odoo10/addons/account_bank_statement_import/
# account_bank_statement_import.py, it's the only 2 keys
# we can provide to match a partner.
payment_ref = transaction.payee
if transaction.checknum:
payment_ref += " " + transaction.checknum
if transaction.memo:
payment_ref += " : " + transaction.memo
vals = {
"date": transaction.date,
"payment_ref": payment_ref,
"amount": float(transaction.amount),
"unique_import_id": transaction.id,
}
return vals
def _parse_file(self, data_file):
ofx = self._check_ofx(data_file)
if not ofx:
return super()._parse_file(data_file)
result = []
try:
for account in ofx.accounts:
transactions = []
total_amt = 0.00
if not account.statement.transactions:
continue
for transaction in account.statement.transactions:
vals = self._prepare_ofx_transaction_line(transaction)
if vals:
transactions.append(vals)
total_amt += vals["amount"]
balance = float(account.statement.balance)
vals_bank_statement = {
"name": account.number,
"transactions": transactions,
"balance_start": balance - total_amt,
"balance_end_real": balance,
}
result.append(
(account.statement.currency, account.number, [vals_bank_statement])
)
except Exception as e:
raise UserError(
_(
"The following problem occurred during import. "
"The file might not be valid.\n\n %s"
)
% str(e)
)
return result