mirror of
https://github.com/OCA/bank-statement-import.git
synced 2025-01-20 12:37:43 +02:00
[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:
@@ -1 +0,0 @@
|
|||||||
from . import account_bank_statement_import
|
|
||||||
@@ -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]
|
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
@@ -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()
|
|
||||||
@@ -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">
|
||||||
1
account_statement_import_ofx/wizard/__init__.py
Normal file
1
account_statement_import_ofx/wizard/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import account_statement_import
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user