mirror of
https://github.com/brain-tec/account_ebics.git
synced 2026-11-06 10:00:39 +00:00
[IMP] account_ebics - Add unit tests
Add 97 tests: - account_ebics: order numbers, camt and CFONB split per account, journal lookup, duplicate statements, download and upload wizards, key initialisation (INI/HIA/HPB), passphrase change, admin orders. - account_ebics_batch: scheduled import, import log, reprocess. - account_ebics_payment_order / account_ebics_batch_payment: upload of the payment file and link to the payment document. - account_ebics_statement_import_test (new, test-only, not to install in production): installs the OCA camt and CFONB parsers in CI and runs the end-to-end statement import. The bank is simulated by patching ebics.xfer._setup_client, or the fintech client classes for the key initialisation; keys, certificates and the INI letter are real, created in a temporary directory.
This commit is contained in:
1 parent
ed0bf5d6ae
commit
a1fc381121
50 files changed
+2075
-5
No files matched your search
+1
-1
@@ -2,7 +2,7 @@
|
||||
_commit: v9.1.0
|
||||
_src_path: git@github.com:Noviat-CI/addons-repo-template.git
|
||||
ci_disable_autoinstall_modules: account_asset
|
||||
community_excluded_modules: account_ebics_batch_payment,account_ebics_oe
|
||||
community_excluded_modules: account_ebics_batch_payment,account_ebics_oe,account_ebics_oe_statement_import_test
|
||||
odoo_test_flavor: Both
|
||||
odoo_version: 18.0
|
||||
org_slug: Noviat
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
name: test with Odoo Community
|
||||
odoo_enterprise : 0
|
||||
exclude_modules : "account_asset"
|
||||
skip: "account_ebics_batch_payment,account_ebics_oe"
|
||||
skip: "account_ebics_batch_payment,account_ebics_oe,account_ebics_oe_statement_import_test"
|
||||
- container: ghcr.io/noviat-ci/odoo-ci:18.0-stable
|
||||
name: test with Odoo Enterprise
|
||||
odoo_enterprise : 1
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
from . import test_ebics_admin_wizards
|
||||
from . import test_ebics_config
|
||||
from . import test_ebics_file
|
||||
from . import test_ebics_file_split
|
||||
from . import test_ebics_userid
|
||||
from . import test_ebics_xfer
|
||||
from . import test_ebics_xfer_client
|
||||
@@ -0,0 +1,198 @@
|
||||
# Copyright 2026 Noviat.
|
||||
# License LGPL-3 or later (https://www.gnu.org/licenses/lgpl).
|
||||
|
||||
import base64
|
||||
from contextlib import contextmanager
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo import Command, fields
|
||||
from odoo.tools import file_open
|
||||
|
||||
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
|
||||
# Synthetic files: structures mirror real bank files, all values are invented.
|
||||
SAMPLES_DIR = "account_ebics/tests/samples"
|
||||
|
||||
# Bank accounts used in the samples (fictitious, checksum-valid).
|
||||
IBAN_CMB = "FR7615589297011000000010111"
|
||||
IBAN_CA833 = [
|
||||
"FR7613306000012000000020197",
|
||||
"FR7613306000012000000020294",
|
||||
"FR7613306000012000000020391",
|
||||
]
|
||||
IBAN_LCL = [
|
||||
"FR7630002004563000000030102",
|
||||
"FR7630002004563000000030296",
|
||||
"FR7630002004563000000030393",
|
||||
"FR7630002004563000000030490",
|
||||
]
|
||||
IBAN_CA879 = [
|
||||
"FR7617906000324000000040136",
|
||||
"FR7617906000324000000040233",
|
||||
"FR7617906000324000000040330",
|
||||
"FR7617906000324000000040427",
|
||||
]
|
||||
IBAN_CIC = "FR7630087331005000000050175"
|
||||
IBAN_HVB = "DE54793200750010000001"
|
||||
IBAN_SPK = "DE52305500000010000002"
|
||||
IBAN_MULTI = ["DE69370400440010000003", "DE42370400440010000004"]
|
||||
IBAN_SUFFIX = "DE15370400440010000005"
|
||||
|
||||
|
||||
def read_sample(name):
|
||||
with file_open(f"{SAMPLES_DIR}/{name}", "rb") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
class FakeEbicsClient:
|
||||
"""Stand-in for fintech.ebics.EbicsClient, returned by a patched
|
||||
ebics.xfer._setup_client so that no keyring and no bank are needed.
|
||||
|
||||
:param downloads: dict order type ("BTD", "FDL" or the EBICS 2 order
|
||||
type) -> bytes (or dict of document name -> bytes) to return.
|
||||
:param error: exception instance raised by every transfer call.
|
||||
"""
|
||||
|
||||
def __init__(self, downloads=None, order_id="N001", error=None):
|
||||
self.downloads = downloads or {}
|
||||
self.order_id = order_id
|
||||
self.error = error
|
||||
self.uploads = []
|
||||
self.confirmed = []
|
||||
self.last_trans_id = "TRANS0001"
|
||||
|
||||
def _download(self, key):
|
||||
if self.error:
|
||||
raise self.error
|
||||
return self.downloads[key]
|
||||
|
||||
def _upload(self, key, data, kwargs):
|
||||
if self.error:
|
||||
raise self.error
|
||||
self.uploads.append((key, data, kwargs))
|
||||
return self.order_id
|
||||
|
||||
def BTD(self, btf, start=None, end=None, **kwargs):
|
||||
return self._download("BTD")
|
||||
|
||||
def FDL(self, filetype, start=None, end=None):
|
||||
return self._download("FDL")
|
||||
|
||||
def download(self, order_type, params=None):
|
||||
return self._download(order_type)
|
||||
|
||||
def BTU(self, btf, data, **kwargs):
|
||||
return self._upload("BTU", data, kwargs)
|
||||
|
||||
def FUL(self, filetype, data, **kwargs):
|
||||
return self._upload("FUL", data, kwargs)
|
||||
|
||||
def upload(self, order_type, data):
|
||||
return self._upload(order_type, data, {})
|
||||
|
||||
def confirm_download(self, trans_id=None, success=True):
|
||||
self.confirmed.append((trans_id, success))
|
||||
|
||||
|
||||
class EbicsTestCommon(AccountTestInvoicingCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env.user.groups_id |= cls.env.ref("account_ebics.group_ebics_manager")
|
||||
cls.eur = cls.env.ref("base.EUR")
|
||||
cls.eur.active = True
|
||||
cls.company = cls.env.company
|
||||
cls.ebics_config = cls.env["ebics.config"].create(
|
||||
{
|
||||
"name": "Test Bank",
|
||||
"ebics_host": "TESTHOST",
|
||||
"ebics_url": "https://ebics.example.invalid/ebics",
|
||||
"ebics_partner": "PARTNER1",
|
||||
"ebics_version": "H004",
|
||||
"state": "confirm",
|
||||
}
|
||||
)
|
||||
cls.ebics_userid = cls.env["ebics.userid"].create(
|
||||
{
|
||||
"name": "USER1",
|
||||
"ebics_config_id": cls.ebics_config.id,
|
||||
"user_ids": [Command.set(cls.env.user.ids)],
|
||||
"company_ids": [Command.set(cls.company.ids)],
|
||||
"ebics_passphrase": "test-passphrase",
|
||||
"ebics_passphrase_store": True,
|
||||
"state": "active_keys",
|
||||
}
|
||||
)
|
||||
cls.ff_camt053 = cls.env.ref("account_ebics.ebics_ff_C53")
|
||||
cls.ff_cfonb120 = cls.env.ref(
|
||||
"account_ebics.ebics_ff_FDL_camt_xxx_cfonb120_stm"
|
||||
)
|
||||
cls.ff_btd_cfonb120 = cls.env.ref("account_ebics.ebics_ff_btf_cfonb120")
|
||||
|
||||
@classmethod
|
||||
def _create_bank_journal(cls, code, acc_number, currency=None):
|
||||
# sudo: accountants may not create bank accounts (res.partner.bank)
|
||||
journal = (
|
||||
cls.env["account.journal"]
|
||||
.sudo()
|
||||
.create(
|
||||
{
|
||||
"name": f"Bank {code}",
|
||||
"code": code,
|
||||
"type": "bank",
|
||||
"bank_acc_number": acc_number,
|
||||
"currency_id": (currency or cls.eur).id,
|
||||
"company_id": cls.company.id,
|
||||
}
|
||||
)
|
||||
.sudo(False)
|
||||
)
|
||||
cls.ebics_config.journal_ids = [Command.link(journal.id)]
|
||||
return journal
|
||||
|
||||
@classmethod
|
||||
def _create_ebics_file(cls, sample, file_format, name=None):
|
||||
return cls.env["ebics.file"].create(
|
||||
{
|
||||
"name": name or sample,
|
||||
"data": base64.b64encode(read_sample(sample)),
|
||||
"date": fields.Datetime.now(),
|
||||
"format_id": file_format.id,
|
||||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _new_res():
|
||||
return {"statement_ids": [], "notifications": []}
|
||||
|
||||
@contextmanager
|
||||
def _fake_client(self, client):
|
||||
xfer_class = type(self.env["ebics.xfer"])
|
||||
with patch.object(xfer_class, "_setup_client", return_value=client):
|
||||
yield client
|
||||
|
||||
def _download_wizard(self, file_format, **vals):
|
||||
return (
|
||||
self.env["ebics.xfer"]
|
||||
.with_context(ebics_download=True)
|
||||
.create(
|
||||
{
|
||||
"ebics_config_id": self.ebics_config.id,
|
||||
"ebics_userid_id": self.ebics_userid.id,
|
||||
"format_id": file_format.id,
|
||||
**vals,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def _upload_wizard(self, file_format, data=b"<Document/>", fname="pay.xml", **vals):
|
||||
return self.env["ebics.xfer"].create(
|
||||
{
|
||||
"ebics_config_id": self.ebics_config.id,
|
||||
"ebics_userid_id": self.ebics_userid.id,
|
||||
"format_id": file_format.id,
|
||||
"upload_data": base64.b64encode(data),
|
||||
"upload_fname": fname,
|
||||
**vals,
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"><BkToCstmrStmt><GrpHdr><MsgId>MSG-TEST-0001</MsgId><CreDtTm>2026-10-13T22:00:00</CreDtTm></GrpHdr><Stmt><Id>SUFFIX-STMT-1</Id><ElctrncSeqNb>1</ElctrncSeqNb><CreDtTm>2026-10-13T22:00:00</CreDtTm><Acct><Id><Othr><Id>0010000005EUR</Id></Othr></Id><Ccy>EUR</Ccy></Acct><Bal><Tp><CdOrPrtry><Cd>OPBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">10.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Bal><Tp><CdOrPrtry><Cd>CLBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">15.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Ntry><NtryRef>SUFFIX-0001</NtryRef><Amt Ccy="EUR">5.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Sts><Cd>BOOK</Cd></Sts><BookgDt><Dt>2026-10-13</Dt></BookgDt><ValDt><Dt>2026-10-13</Dt></ValDt><AcctSvcrRef>SUFFIX-0001</AcctSvcrRef><BkTxCd><Domn><Cd>PMNT</Cd><Fmly><Cd>ICDT</Cd><SubFmlyCd>ESCT</SubFmlyCd></Fmly></Domn><Prtry><Cd>NTRF+116</Cd><Issr>DK</Issr></Prtry></BkTxCd><NtryDtls><TxDtls><Refs><AcctSvcrRef>SUFFIX-0001</AcctSvcrRef></Refs><Amt Ccy="EUR">5.00</Amt><RmtInf><Ustrd>Rho refund</Ustrd></RmtInf></TxDtls></NtryDtls></Ntry></Stmt></BkToCstmrStmt></Document>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.08"><BkToCstmrStmt><GrpHdr><MsgId>MSG-TEST-0001</MsgId><CreDtTm>2026-10-13T22:00:00</CreDtTm></GrpHdr><Stmt><Id>HVB-STMT-0001</Id><ElctrncSeqNb>1</ElctrncSeqNb><CreDtTm>2026-10-13T22:00:00</CreDtTm><Acct><Id><IBAN>DE54793200750010000001</IBAN></Id><Ccy>EUR</Ccy></Acct><Bal><Tp><CdOrPrtry><Cd>OPBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">5000.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Bal><Tp><CdOrPrtry><Cd>CLBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">3992.50</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Bal><Tp><CdOrPrtry><Cd>CLAV</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">3992.50</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Ntry><NtryRef>HVB-0001</NtryRef><Amt Ccy="EUR">150.00</Amt><CdtDbtInd>DBIT</CdtDbtInd><Sts><Cd>BOOK</Cd></Sts><BookgDt><Dt>2026-10-13</Dt></BookgDt><ValDt><Dt>2026-10-13</Dt></ValDt><AcctSvcrRef>HVB-0001</AcctSvcrRef><BkTxCd><Domn><Cd>PMNT</Cd><Fmly><Cd>ICDT</Cd><SubFmlyCd>ESCT</SubFmlyCd></Fmly></Domn><Prtry><Cd>NTRF+116</Cd><Issr>DK</Issr></Prtry></BkTxCd><NtryDtls><TxDtls><Refs><AcctSvcrRef>HVB-0001</AcctSvcrRef><EndToEndId>E2E-LAMBDA-118</EndToEndId></Refs><Amt Ccy="EUR">150.00</Amt><RmtInf><Ustrd>Rechnung 2026-118 Lambda GmbH</Ustrd></RmtInf></TxDtls></NtryDtls></Ntry><Ntry><NtryRef>HVB-0002</NtryRef><Amt Ccy="EUR">857.50</Amt><CdtDbtInd>DBIT</CdtDbtInd><Sts><Cd>BOOK</Cd></Sts><BookgDt><Dt>2026-10-13</Dt></BookgDt><ValDt><Dt>2026-10-13</Dt></ValDt><AcctSvcrRef>HVB-0002</AcctSvcrRef><BkTxCd><Domn><Cd>PMNT</Cd><Fmly><Cd>ICDT</Cd><SubFmlyCd>ESCT</SubFmlyCd></Fmly></Domn><Prtry><Cd>NTRF+116</Cd><Issr>DK</Issr></Prtry></BkTxCd><NtryDtls><TxDtls><Refs><AcctSvcrRef>HVB-0002</AcctSvcrRef></Refs><Amt Ccy="EUR">857.50</Amt><AmtDtls><InstdAmt><Amt Ccy="USD">1000.00</Amt></InstdAmt></AmtDtls><Chrgs><TtlChrgsAndTaxAmt Ccy="EUR">12.50</TtlChrgsAndTaxAmt></Chrgs><RmtInf><Ustrd>Zahlung USD Mu Inc</Ustrd></RmtInf></TxDtls></NtryDtls></Ntry></Stmt></BkToCstmrStmt></Document>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"><BkToCstmrStmt><GrpHdr><MsgId>MSG-TEST-0001</MsgId><CreDtTm>2026-10-13T22:00:00</CreDtTm></GrpHdr><Stmt><Id>MULTI-STMT-1</Id><ElctrncSeqNb>1</ElctrncSeqNb><CreDtTm>2026-10-13T22:00:00</CreDtTm><Acct><Id><IBAN>DE69370400440010000003</IBAN></Id><Ccy>EUR</Ccy></Acct><Bal><Tp><CdOrPrtry><Cd>OPBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">100.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Bal><Tp><CdOrPrtry><Cd>CLBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">130.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Ntry><NtryRef>MULTI-0001</NtryRef><Amt Ccy="EUR">30.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Sts><Cd>BOOK</Cd></Sts><BookgDt><Dt>2026-10-13</Dt></BookgDt><ValDt><Dt>2026-10-13</Dt></ValDt><AcctSvcrRef>MULTI-0001</AcctSvcrRef><BkTxCd><Domn><Cd>PMNT</Cd><Fmly><Cd>ICDT</Cd><SubFmlyCd>ESCT</SubFmlyCd></Fmly></Domn><Prtry><Cd>NTRF+116</Cd><Issr>DK</Issr></Prtry></BkTxCd><NtryDtls><TxDtls><Refs><AcctSvcrRef>MULTI-0001</AcctSvcrRef></Refs><Amt Ccy="EUR">30.00</Amt><RmtInf><Ustrd>Omikron payment</Ustrd></RmtInf></TxDtls></NtryDtls></Ntry></Stmt><Stmt><Id>MULTI-STMT-2</Id><ElctrncSeqNb>1</ElctrncSeqNb><CreDtTm>2026-10-13T22:00:00</CreDtTm><Acct><Id><IBAN>DE42370400440010000004</IBAN></Id><Ccy>EUR</Ccy></Acct><Bal><Tp><CdOrPrtry><Cd>OPBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">200.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Bal><Tp><CdOrPrtry><Cd>CLBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">180.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Ntry><NtryRef>MULTI-0002</NtryRef><Amt Ccy="EUR">20.00</Amt><CdtDbtInd>DBIT</CdtDbtInd><Sts><Cd>BOOK</Cd></Sts><BookgDt><Dt>2026-10-13</Dt></BookgDt><ValDt><Dt>2026-10-13</Dt></ValDt><AcctSvcrRef>MULTI-0002</AcctSvcrRef><BkTxCd><Domn><Cd>PMNT</Cd><Fmly><Cd>ICDT</Cd><SubFmlyCd>ESCT</SubFmlyCd></Fmly></Domn><Prtry><Cd>NTRF+116</Cd><Issr>DK</Issr></Prtry></BkTxCd><NtryDtls><TxDtls><Refs><AcctSvcrRef>MULTI-0002</AcctSvcrRef></Refs><Amt Ccy="EUR">20.00</Amt><RmtInf><Ustrd>Pi fee</Ustrd></RmtInf></TxDtls></NtryDtls></Ntry></Stmt></BkToCstmrStmt></Document>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.08"><BkToCstmrStmt><GrpHdr><MsgId>MSG-TEST-0001</MsgId><CreDtTm>2026-10-13T22:00:00</CreDtTm></GrpHdr><Stmt><Id>NOACC-STMT-1</Id><ElctrncSeqNb>1</ElctrncSeqNb><CreDtTm>2026-10-13T22:00:00</CreDtTm><Acct><Ccy>EUR</Ccy></Acct><Bal><Tp><CdOrPrtry><Cd>OPBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">1.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Bal><Tp><CdOrPrtry><Cd>CLBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">2.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Ntry><NtryRef>NOACC-0001</NtryRef><Amt Ccy="EUR">1.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Sts><Cd>BOOK</Cd></Sts><BookgDt><Dt>2026-10-13</Dt></BookgDt><ValDt><Dt>2026-10-13</Dt></ValDt><AcctSvcrRef>NOACC-0001</AcctSvcrRef><BkTxCd><Domn><Cd>PMNT</Cd><Fmly><Cd>ICDT</Cd><SubFmlyCd>ESCT</SubFmlyCd></Fmly></Domn><Prtry><Cd>NTRF+116</Cd><Issr>DK</Issr></Prtry></BkTxCd><NtryDtls><TxDtls><Refs><AcctSvcrRef>NOACC-0001</AcctSvcrRef></Refs><Amt Ccy="EUR">1.00</Amt><RmtInf><Ustrd>Sigma</Ustrd></RmtInf></TxDtls></NtryDtls></Ntry></Stmt></BkToCstmrStmt></Document>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.08"><BkToCstmrStmt><GrpHdr><MsgId>MSG-TEST-0001</MsgId><CreDtTm>2026-10-13T22:00:00</CreDtTm></GrpHdr><Stmt><Id>EMPTY-STMT-1</Id><ElctrncSeqNb>1</ElctrncSeqNb><CreDtTm>2026-10-13T22:00:00</CreDtTm><Acct><Id><IBAN>DE52305500000010000002</IBAN></Id><Ccy>EUR</Ccy></Acct><Bal><Tp><CdOrPrtry><Cd>OPBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">720.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Bal><Tp><CdOrPrtry><Cd>CLBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">720.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal></Stmt></BkToCstmrStmt></Document>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.08"><BkToCstmrStmt><GrpHdr><MsgId>MSG-TEST-0001</MsgId><CreDtTm>2026-10-13T22:00:00</CreDtTm></GrpHdr><Stmt><Id>SPK-STMT-0001</Id><ElctrncSeqNb>1</ElctrncSeqNb><CreDtTm>2026-10-13T22:00:00</CreDtTm><Acct><Id><IBAN>DE52305500000010000002</IBAN></Id><Ccy>EUR</Ccy></Acct><Bal><Tp><CdOrPrtry><Cd>OPBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">720.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Bal><Tp><CdOrPrtry><Cd>CLBD</Cd></CdOrPrtry></Tp><Amt Ccy="EUR">1065.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Dt><Dt>2026-10-13</Dt></Dt></Bal><Ntry><NtryRef>SPK-0001</NtryRef><Amt Ccy="EUR">400.00</Amt><CdtDbtInd>CRDT</CdtDbtInd><Sts><Cd>BOOK</Cd></Sts><BookgDt><Dt>2026-10-13</Dt></BookgDt><ValDt><Dt>2026-10-13</Dt></ValDt><AcctSvcrRef>SPK-0001</AcctSvcrRef><BkTxCd><Domn><Cd>PMNT</Cd><Fmly><Cd>ICDT</Cd><SubFmlyCd>ESCT</SubFmlyCd></Fmly></Domn><Prtry><Cd>NTRF+116</Cd><Issr>DK</Issr></Prtry></BkTxCd><NtryDtls><TxDtls><Refs><AcctSvcrRef>SPK-0001</AcctSvcrRef></Refs><Amt Ccy="EUR">400.00</Amt><RmtInf><Ustrd>Gutschrift Nu KG</Ustrd></RmtInf></TxDtls></NtryDtls></Ntry><Ntry><NtryRef>SPK-0002</NtryRef><Amt Ccy="EUR">55.00</Amt><CdtDbtInd>DBIT</CdtDbtInd><Sts><Cd>BOOK</Cd></Sts><BookgDt><Dt>2026-10-13</Dt></BookgDt><ValDt><Dt>2026-10-13</Dt></ValDt><AcctSvcrRef>SPK-0002</AcctSvcrRef><BkTxCd><Domn><Cd>PMNT</Cd><Fmly><Cd>ICDT</Cd><SubFmlyCd>ESCT</SubFmlyCd></Fmly></Domn><Prtry><Cd>NTRF+116</Cd><Issr>DK</Issr></Prtry></BkTxCd><NtryDtls><TxDtls><Refs><AcctSvcrRef>SPK-0002</AcctSvcrRef></Refs><Amt Ccy="EUR">55.00</Amt><RmtInf><Ustrd>Lastschrift Xi Versicherung</Ustrd></RmtInf></TxDtls></NtryDtls></Ntry></Stmt></BkToCstmrStmt></Document>
|
||||
@@ -0,0 +1 @@
|
||||
0115589 29701EUR2 10000000101 131026 0000000015000{
|
||||
@@ -0,0 +1,10 @@
|
||||
0117906 00032EUR2 40000000401 131026** COMPTE COURANT 0000000000100{
|
||||
0717906 00032EUR2 40000000401 131026--------------------------------------------------0000000000100{
|
||||
0117906 00032EUR2 40000000402 131026** COMPTE EPARGNE 0000000000200{
|
||||
0717906 00032EUR2 40000000402 131026--------------------------------------------------0000000000200{
|
||||
0117906 00032EUR2 40000000403 131026 0000000000300{
|
||||
0717906 00032EUR2 40000000403 131026 0000000000300{
|
||||
0117906 00032EUR2 40000000404 131026 0000000040000{
|
||||
0417906000000032EUR2 4000000040405131026 131026VIR ETA SA 0000000 0000000015000{
|
||||
0417906000000032EUR2 4000000040405131026 131026PRLV THETA ASSURANCES 0000000 0000000002100}
|
||||
0717906 00032EUR2 40000000404 131026 0000000052900{
|
||||
@@ -0,0 +1,14 @@
|
||||
0113306 00001EUR2 20000000201 131026 0000000084000{
|
||||
0413306000000001EUR2 2000000020105131026 131026VIR SEPA RECU GAMMA SARL 0000000 0000000006400{
|
||||
0513306000000001EUR2 2000000020105131026 NBEGAMMA SARL
|
||||
0513306000000001EUR2 2000000020105131026 LCCFACT 99812
|
||||
0513306000000001EUR2 2000000020105131026 REFE2E-GAMMA-1
|
||||
0413306000000001EUR2 2000000020105131026 131026PRLV SEPA DELTA ENERGIE 0000000 0000000001325N
|
||||
0513306000000001EUR2 2000000020105131026 NPYDELTA ENERGIE
|
||||
0513306000000001EUR2 2000000020105131026 RUMRUM-DELTA-0042
|
||||
0713306 00001EUR2 20000000201 131026 0000000089074E
|
||||
0113306 00001EUR2 20000000202 131026 0000000012345{
|
||||
0713306 00001EUR2 20000000202 131026 0000000012345{
|
||||
0113306 00001EUR2 20000000203 131026 0000000000500{
|
||||
0413306000000001EUR2 2000000020305131026 131026FRAIS TENUE DE COMPTE 0000000 0000000000099}
|
||||
0713306 00001EUR2 20000000203 131026 0000000000401{
|
||||
@@ -0,0 +1,14 @@
|
||||
0130087 33100EUR2 50000000501 131026 0000000009900{
|
||||
0430087000033100EUR2 5000000050105131026 131026VIR SEPA IOTA DISTRIBUTION 0000000 0000000021500{
|
||||
0530087000033100EUR2 5000000050105131026 NOMIOTA DISTRIBUTION
|
||||
0530087000033100EUR2 5000000050105131026 RIB30087331005000000050199
|
||||
0530087000033100EUR2 5000000050105131026 IBEFR7630087331005000000050199
|
||||
0530087000033100EUR2 5000000050105131026 CBECMCIFRPP
|
||||
0530087000033100EUR2 5000000050105131026 LCCFACTURE 7731
|
||||
0530087000033100EUR2 5000000050105131026 RCNSALE
|
||||
0530087000033100EUR2 5000000050105131026 REFE2E-IOTA-7731
|
||||
0430087000033100EUR2 5000000050105131026 131026PRLV SEPA KAPPA TELECOM 0000000 0000000000499R
|
||||
0530087000033100EUR2 5000000050105131026 NPYKAPPA TELECOM
|
||||
0530087000033100EUR2 5000000050105131026 RUMRUM-KAPPA-1
|
||||
0530087000033100EUR2 5000000050105131026 LIBABONNEMENT
|
||||
0730087 33100EUR2 50000000501 131026 0000000030900A
|
||||
@@ -0,0 +1 @@
|
||||
0115589 29701EUR2 10000000101 131026 0000000015000{ 0715589 29701EUR2 10000000101 131026 0000000015000{
|
||||
@@ -0,0 +1 @@
|
||||
0130002 00456EUR2 30000000301 131026 0000000001000{ 0730002 00456EUR2 30000000301 131026 0000000001000{ 0130002 00456EUR2 30000000302 131026 0000000025000{ 0430002000000456EUR2 3000000030205131026 131026VIR SEPA EPSILON SAS 0000000 0000000007800{ 0530002000000456EUR2 3000000030205131026 LIBACOMPTE CHANTIER 12 0430002000000456EUR2 3000000030205131026 131026CB STATION ZETA 0000000 0000000000612} 0730002 00456EUR2 30000000302 131026 0000000032188{ 0130002 00456EUR2 30000000303 131026 0000000000000{ 0730002 00456EUR2 30000000303 131026 0000000000000{ 0130002 00456EUR2 30000000304 131026 0000000003200{ 0430002000000456EUR2 3000000030405131026 131026REMISE CHEQUES 0000000 0000000004500{ 0730002 00456EUR2 30000000304 131026 0000000007700{
|
||||
@@ -0,0 +1 @@
|
||||
0115589 29701EUR2 10000000101 131026 0000000015000{ 0415589000029701EUR2 1000000010105131026 131026PRLV SEPA FOURNISSEUR ALPHA 0000000 0000000002504} 0515589000029701EUR2 1000000010105131026 LIBFACTURE 2026-0412 0515589000029701EUR2 1000000010105131026 RCNREF ALPHA 77881 0415589000029701EUR2 1000000010105131026 131026VIR SEPA CLIENT BETA 0000000 0000000012000{ 0515589000029701EUR2 1000000010105131026 LCCCOMMANDE 5521 0715589 29701EUR2 10000000101 131026 0000000024496{
|
||||
@@ -0,0 +1 @@
|
||||
this is not an xml document
|
||||
@@ -0,0 +1,3 @@
|
||||
Kundenprotokoll (PTK)
|
||||
Auftragsart: C53 Auftragsnummer: A00T Teilnehmer: TESTUSER1
|
||||
Status: Auftrag erfolgreich verarbeitet
|
||||
@@ -0,0 +1,152 @@
|
||||
# Copyright 2026 Noviat.
|
||||
# License LGPL-3 or later (https://www.gnu.org/licenses/lgpl).
|
||||
|
||||
import pprint
|
||||
from unittest.mock import patch
|
||||
|
||||
from fintech.ebics import EbicsFunctionalError, EbicsKeyRing, EbicsTechnicalError
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import tagged
|
||||
|
||||
from .test_ebics_userid import EbicsKeysCommon
|
||||
|
||||
# Test passphrases for throwaway keys created in a temporary directory.
|
||||
INIT_PHRASE = "init-passphrase"
|
||||
NEXT_PHRASE = "next-passphrase"
|
||||
OTHER_PHRASE = "other-passphrase"
|
||||
WRONG_PHRASE = "wrong-passphrase"
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestChangePassphrase(EbicsKeysCommon):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.userid = self._new_userid()
|
||||
self._init_1(self.userid)
|
||||
|
||||
def _change(self, **vals):
|
||||
wizard = self.env["ebics.change.passphrase"].create(
|
||||
{"ebics_userid_id": self.userid.id, **vals}
|
||||
)
|
||||
wizard.change_passphrase()
|
||||
return wizard
|
||||
|
||||
def test_change_passphrase(self):
|
||||
wizard = self._change(
|
||||
old_pass=INIT_PHRASE,
|
||||
new_pass=NEXT_PHRASE,
|
||||
new_pass_check=NEXT_PHRASE,
|
||||
)
|
||||
self.assertEqual(self.userid.ebics_passphrase, NEXT_PHRASE)
|
||||
self.assertIn("has been changed", wizard.note)
|
||||
keyring = EbicsKeyRing(keys=self.userid.ebics_keys_fn, passphrase=NEXT_PHRASE)
|
||||
self.assertTrue(keyring["#USER"])
|
||||
|
||||
def test_wrong_old_passphrase(self):
|
||||
with self.assertRaisesRegex(UserError, "Incorrect old passphrase"):
|
||||
self._change(
|
||||
old_pass=WRONG_PHRASE,
|
||||
new_pass=NEXT_PHRASE,
|
||||
new_pass_check=NEXT_PHRASE,
|
||||
)
|
||||
|
||||
def test_verification_mismatch(self):
|
||||
with self.assertRaisesRegex(UserError, "verification error"):
|
||||
self._change(new_pass=NEXT_PHRASE, new_pass_check=OTHER_PHRASE)
|
||||
|
||||
def test_new_equals_old(self):
|
||||
with self.assertRaisesRegex(UserError, "equal to old"):
|
||||
self._change(new_pass=INIT_PHRASE, new_pass_check=INIT_PHRASE)
|
||||
|
||||
def test_wrong_old_passphrase_not_stored(self):
|
||||
"""Without a stored passphrase, fintech rejects the wrong one."""
|
||||
self.userid.ebics_passphrase_store = False
|
||||
self.userid.ebics_passphrase = False
|
||||
with self.assertRaisesRegex(UserError, "(?i)passphrase"):
|
||||
self._change(
|
||||
old_pass=WRONG_PHRASE,
|
||||
new_pass=NEXT_PHRASE,
|
||||
new_pass_check=NEXT_PHRASE,
|
||||
)
|
||||
|
||||
def test_change_passphrase_not_stored(self):
|
||||
"""The new passphrase must not be written when storing is off."""
|
||||
self.userid.ebics_passphrase_store = False
|
||||
self.userid.ebics_passphrase = False
|
||||
wizard = self._change(
|
||||
old_pass=INIT_PHRASE,
|
||||
new_pass=NEXT_PHRASE,
|
||||
new_pass_check=NEXT_PHRASE,
|
||||
)
|
||||
self.assertFalse(self.userid.ebics_passphrase)
|
||||
self.assertIn("has been changed", wizard.note)
|
||||
keyring = EbicsKeyRing(keys=self.userid.ebics_keys_fn, passphrase=NEXT_PHRASE)
|
||||
self.assertTrue(keyring["#USER"])
|
||||
|
||||
|
||||
class FakeAdminClient:
|
||||
def __init__(self, error=None):
|
||||
self.error = error
|
||||
|
||||
def _order(self, name):
|
||||
if self.error:
|
||||
raise self.error
|
||||
return {"order": name, "Services": [{"ServiceName": "SCT"}]}
|
||||
|
||||
def HAA(self, parsed=False):
|
||||
return self._order("HAA")
|
||||
|
||||
def HPD(self, parsed=False):
|
||||
return self._order("HPD")
|
||||
|
||||
def HKD(self, parsed=False):
|
||||
return self._order("HKD")
|
||||
|
||||
def HTD(self, parsed=False):
|
||||
return self._order("HTD")
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestAdminOrder(EbicsKeysCommon):
|
||||
def _run(self, order_type, client):
|
||||
wizard = self.env["ebics.admin.order"].create(
|
||||
{
|
||||
"ebics_config_id": self.ebics_config.id,
|
||||
"ebics_userid_id": self.ebics_userid.id,
|
||||
"admin_order_type": order_type,
|
||||
}
|
||||
)
|
||||
wizard_class = type(wizard)
|
||||
with patch.object(wizard_class, "_setup_client", return_value=client):
|
||||
wizard.ebics_admin_order()
|
||||
return wizard
|
||||
|
||||
def test_admin_orders(self):
|
||||
for order_type in ("HAA", "HPD", "HKD", "HTD"):
|
||||
with self.subTest(order_type=order_type):
|
||||
wizard = self._run(order_type, FakeAdminClient())
|
||||
expected = pprint.PrettyPrinter().pformat(
|
||||
FakeAdminClient()._order(order_type)
|
||||
)
|
||||
self.assertEqual(wizard.note, expected)
|
||||
|
||||
def test_admin_order_errors(self):
|
||||
cases = [
|
||||
(EbicsTechnicalError("061001", "EBICS_AUTHENTICATION_FAILED"), "Technical"),
|
||||
(
|
||||
EbicsFunctionalError("091005", "EBICS_NO_DOWNLOAD_DATA_AVAILABLE"),
|
||||
"Functional",
|
||||
),
|
||||
]
|
||||
for error, expected in cases:
|
||||
with self.subTest(error=type(error).__name__):
|
||||
wizard = self._run("HAA", FakeAdminClient(error=error))
|
||||
self.assertIn(
|
||||
f"EBICS {expected} Error during execution of order HAA", wizard.note
|
||||
)
|
||||
self.assertIn(f"(code: {error.code})", wizard.note)
|
||||
|
||||
def test_admin_order_client_setup_failure(self):
|
||||
wizard = self._run("HPD", False)
|
||||
self.assertIn("client setup failed", wizard.note)
|
||||
@@ -0,0 +1,52 @@
|
||||
# Copyright 2026 Noviat.
|
||||
# License LGPL-3 or later (https://www.gnu.org/licenses/lgpl).
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import tagged
|
||||
|
||||
from .common import EbicsTestCommon
|
||||
|
||||
IBAN_FOR_COMPANY_TEST = "FR7630004000031234567890143"
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestEbicsConfig(EbicsTestCommon):
|
||||
def test_update_order_number(self):
|
||||
cases = [
|
||||
("A000", "A001"),
|
||||
("A009", "A00A"),
|
||||
("A00Z", "A010"),
|
||||
("A0Z9", "A0ZA"),
|
||||
("AZZZ", "B000"),
|
||||
("ZZZY", "A000"),
|
||||
# ZZZZ is accepted by the constraint, it must wrap as well
|
||||
("ZZZZ", "A000"),
|
||||
]
|
||||
for current, expected in cases:
|
||||
with self.subTest(current=current):
|
||||
self.ebics_config._update_order_number(current)
|
||||
self.assertEqual(self.ebics_config.order_number, expected)
|
||||
|
||||
def test_order_number_pattern(self):
|
||||
for valid in ("A000", "B1Z9", "ZZZZ"):
|
||||
with self.subTest(valid=valid):
|
||||
self.ebics_config.order_number = valid
|
||||
# No 5-character case: Char(size=4) truncates before the constraint runs.
|
||||
for invalid in ("a001", "A01", "1000", "A-01"):
|
||||
with self.subTest(invalid=invalid), self.assertRaises(UserError):
|
||||
self.ebics_config.order_number = invalid
|
||||
|
||||
def test_unlink_confirmed_config_is_blocked(self):
|
||||
with self.assertRaises(UserError):
|
||||
self.ebics_config.unlink()
|
||||
|
||||
def test_unlink_draft_config(self):
|
||||
self.ebics_userid.unlink()
|
||||
self.ebics_config.set_to_draft()
|
||||
self.ebics_config.unlink()
|
||||
self.assertFalse(self.ebics_config.exists())
|
||||
|
||||
def test_company_ids_follow_journals(self):
|
||||
journal = self._create_bank_journal("TCMP", IBAN_FOR_COMPANY_TEST)
|
||||
self.assertEqual(self.ebics_config.company_ids, journal.company_id)
|
||||
self.assertEqual(journal.ebics_config_id, self.ebics_config)
|
||||
@@ -0,0 +1,170 @@
|
||||
# Copyright 2026 Noviat.
|
||||
# License LGPL-3 or later (https://www.gnu.org/licenses/lgpl).
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo import Command
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import tagged
|
||||
|
||||
from .common import IBAN_SPK, EbicsTestCommon
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestEbicsFile(EbicsTestCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.journal = cls._create_bank_journal("TSPK", IBAN_SPK)
|
||||
|
||||
def _statement(self, name, date="2026-10-13", import_format="camt053"):
|
||||
statement = self.env["account.bank.statement"].create(
|
||||
{
|
||||
"name": name,
|
||||
"journal_id": self.journal.id,
|
||||
"line_ids": [
|
||||
Command.create(
|
||||
{
|
||||
"journal_id": self.journal.id,
|
||||
"date": date,
|
||||
"payment_ref": f"line {name}",
|
||||
"amount": 10.0,
|
||||
}
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
statement.import_format = import_format
|
||||
return statement
|
||||
|
||||
def test_duplicate_statement_is_removed(self):
|
||||
self._statement("ST-1")
|
||||
duplicate = self._statement("ST-1")
|
||||
res = self._new_res()
|
||||
res["statement_ids"] = duplicate.ids
|
||||
ebics_file = self._create_ebics_file("camt053_sparkasse.xml", self.ff_camt053)
|
||||
kept = ebics_file._statement_duplicate_check(res, duplicate)
|
||||
self.assertFalse(kept)
|
||||
self.assertFalse(duplicate.exists())
|
||||
self.assertEqual(res["statement_ids"], [])
|
||||
self.assertEqual(res["notifications"][0]["type"], "warning")
|
||||
self.assertIn("ST-1", res["notifications"][0]["message"])
|
||||
|
||||
def test_duplicate_check_scope(self):
|
||||
"""A checked statement only collides with the same name, date and format."""
|
||||
# existing statements: same name, other date / same name, other format
|
||||
self._statement("ST-2", date="2026-10-14")
|
||||
self._statement("ST-2", import_format="cfonb120")
|
||||
new = self._statement("ST-2")
|
||||
# outside DUP_CHECK_FORMATS: never checked
|
||||
unchecked = self._statement("ST-2", import_format="camt052")
|
||||
statements = new | unchecked
|
||||
res = self._new_res()
|
||||
res["statement_ids"] = statements.ids
|
||||
ebics_file = self._create_ebics_file("camt053_sparkasse.xml", self.ff_camt053)
|
||||
kept = ebics_file._statement_duplicate_check(res, statements)
|
||||
self.assertEqual(kept, statements)
|
||||
self.assertFalse(res["notifications"])
|
||||
|
||||
def test_process_result_note(self):
|
||||
ebics_file = self._create_ebics_file("camt053_sparkasse.xml", self.ff_camt053)
|
||||
ebics_file.note_process = ""
|
||||
statement = self._statement("ST-3")
|
||||
res = {
|
||||
"statement_ids": statement.ids,
|
||||
"notifications": [
|
||||
{"type": "error", "message": "Broken entry", "details": "line 4"},
|
||||
{"type": "warning", "message": "Odd balance"},
|
||||
{"info": "Something to know"},
|
||||
"Plain text warning",
|
||||
],
|
||||
}
|
||||
ebics_file._process_download_result(res, file_format="camt053")
|
||||
note = ebics_file.note_process
|
||||
self.assertIn("Broken entry\nline 4", note)
|
||||
self.assertIn("Number of errors: 1", note)
|
||||
self.assertIn("Number of warnings: 3", note)
|
||||
self.assertIn("1 bank statement has been imported", note)
|
||||
self.assertEqual(ebics_file.bank_statement_ids, statement)
|
||||
self.assertEqual(statement.import_format, "camt053")
|
||||
self.assertEqual(ebics_file.company_ids, self.company)
|
||||
|
||||
def test_process_result_empty_file(self):
|
||||
ebics_file = self._create_ebics_file("camt053_sparkasse.xml", self.ff_camt053)
|
||||
ebics_file.note_process = ""
|
||||
ebics_file._process_download_result(self._new_res(), file_format="camt053")
|
||||
self.assertIn("doesn't contain any transaction", ebics_file.note_process)
|
||||
|
||||
def test_unlink_done_file_is_blocked(self):
|
||||
ebics_file = self._create_ebics_file("camt053_sparkasse.xml", self.ff_camt053)
|
||||
ebics_file.set_to_done()
|
||||
with self.assertRaises(UserError):
|
||||
ebics_file.unlink()
|
||||
|
||||
def test_unlink_removes_statements(self):
|
||||
ebics_file = self._create_ebics_file("camt053_sparkasse.xml", self.ff_camt053)
|
||||
statement = self._statement("ST-4")
|
||||
statement.ebics_file_id = ebics_file
|
||||
ebics_file.unlink()
|
||||
self.assertFalse(statement.exists())
|
||||
|
||||
def test_unlink_pain002_file(self):
|
||||
"""pain.002 processing is a placeholder: its files must stay removable."""
|
||||
ebics_file = self._create_ebics_file(
|
||||
"ptk_protocol.txt", self.env.ref("account_ebics.ebics_ff_Z01")
|
||||
)
|
||||
ebics_file.unlink()
|
||||
self.assertFalse(ebics_file.exists())
|
||||
|
||||
def test_process_format_without_method(self):
|
||||
"""Modelled on a Sparkasse customer protocol (PTK): stored, not processed."""
|
||||
ptk = self.env["ebics.file.format"].create(
|
||||
{"name": "PTK", "type": "down", "order_type": "PTK", "suffix": "txt"}
|
||||
)
|
||||
ebics_file = self._create_ebics_file("ptk_protocol.txt", ptk)
|
||||
with self.assertRaisesRegex(UserError, "no support"):
|
||||
ebics_file.process()
|
||||
|
||||
def test_check_import_module(self):
|
||||
ebics_file = self._create_ebics_file("camt053_sparkasse.xml", self.ff_camt053)
|
||||
self.assertTrue(ebics_file._check_import_module("account_ebics"))
|
||||
self.assertFalse(
|
||||
ebics_file._check_import_module("no_such_module", raise_if_not_found=False)
|
||||
)
|
||||
with self.assertRaises(UserError):
|
||||
ebics_file._check_import_module("no_such_module")
|
||||
|
||||
def test_enterprise_import_does_not_commit_in_tests(self):
|
||||
"""The Enterprise path commits after each statement in production."""
|
||||
ebics_file = self._create_ebics_file("camt053_sparkasse.xml", self.ff_camt053)
|
||||
statement = self._statement("ST-5")
|
||||
st_datas = [{"acc_number": "0010000002", "journal_id": self.journal.id}]
|
||||
|
||||
def create_statement(res, st_data):
|
||||
res["statement_ids"].append(statement.id)
|
||||
|
||||
file_class = type(ebics_file)
|
||||
cursor_class = type(self.env.cr)
|
||||
res = self._new_res()
|
||||
with (
|
||||
patch.object(
|
||||
file_class, "_create_bank_statement_oe", side_effect=create_statement
|
||||
),
|
||||
patch.object(cursor_class, "commit", side_effect=AssertionError("commit")),
|
||||
):
|
||||
ebics_file._process_bank_statement_oe(res, st_datas)
|
||||
self.assertEqual(res["statement_ids"], statement.ids)
|
||||
self.assertFalse(res["notifications"])
|
||||
|
||||
def test_enterprise_import_reports_errors(self):
|
||||
ebics_file = self._create_ebics_file("camt053_sparkasse.xml", self.ff_camt053)
|
||||
st_datas = [{"acc_number": "0010000002", "journal_id": self.journal.id}]
|
||||
res = self._new_res()
|
||||
with patch.object(
|
||||
type(ebics_file),
|
||||
"_create_bank_statement_oe",
|
||||
side_effect=UserError("Journal mismatch"),
|
||||
):
|
||||
ebics_file._process_bank_statement_oe(res, st_datas)
|
||||
self.assertIn("Journal mismatch", res["notifications"][0]["message"])
|
||||
self.assertIn("0010000002", res["notifications"][0]["message"])
|
||||
@@ -0,0 +1,232 @@
|
||||
# Copyright 2026 Noviat.
|
||||
# License LGPL-3 or later (https://www.gnu.org/licenses/lgpl).
|
||||
|
||||
import base64
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from odoo.tests import tagged
|
||||
|
||||
from .common import (
|
||||
IBAN_CA833,
|
||||
IBAN_CA879,
|
||||
IBAN_CIC,
|
||||
IBAN_CMB,
|
||||
IBAN_HVB,
|
||||
IBAN_LCL,
|
||||
IBAN_MULTI,
|
||||
IBAN_SPK,
|
||||
IBAN_SUFFIX,
|
||||
EbicsTestCommon,
|
||||
)
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestSplitCfonb(EbicsTestCommon):
|
||||
"""EBICS files may hold several accounts and empty statements: they are
|
||||
split per statement before being handed to the CFONB parser."""
|
||||
|
||||
def _split(self, sample):
|
||||
ebics_file = self._create_ebics_file(sample, self.ff_cfonb120)
|
||||
res = self._new_res()
|
||||
return ebics_file._split_cfonb(res), res
|
||||
|
||||
def _lines(self, st_data):
|
||||
return base64.b64decode(st_data["data"]).decode("latin-1").splitlines()
|
||||
|
||||
def test_no_line_terminator_single_account(self):
|
||||
"""Modelled on Crédit Mutuel de Bretagne: no line terminator at all."""
|
||||
journal = self._create_bank_journal("TCMB", IBAN_CMB)
|
||||
datas, res = self._split("cfonb120_no_eol_single_account.dat")
|
||||
self.assertFalse(res["notifications"])
|
||||
self.assertEqual(len(datas), 1)
|
||||
self.assertEqual(datas[0]["acc_number"], "10000000101")
|
||||
self.assertEqual(datas[0]["journal_id"], journal.id)
|
||||
self.assertEqual(datas[0]["company_id"], self.company.id)
|
||||
lines = self._lines(datas[0])
|
||||
self.assertEqual(
|
||||
[x[:2] for x in lines], ["01", "04", "05", "05", "04", "05", "07"]
|
||||
)
|
||||
self.assertTrue(all(len(x) == 120 for x in lines))
|
||||
|
||||
def test_empty_statements_only(self):
|
||||
self._create_bank_journal("TCMB", IBAN_CMB)
|
||||
datas, res = self._split("cfonb120_no_eol_empty_only.dat")
|
||||
self.assertEqual(datas, [])
|
||||
self.assertFalse(res["notifications"])
|
||||
|
||||
def test_multi_account_with_complement_lines(self):
|
||||
"""Modelled on Crédit Agricole: 3 accounts, the middle one empty, and a
|
||||
positive-zero overpunch ('{') on its balance."""
|
||||
j1 = self._create_bank_journal("TCA1", IBAN_CA833[0])
|
||||
j3 = self._create_bank_journal("TCA3", IBAN_CA833[2])
|
||||
datas, res = self._split("cfonb120_lf_multi_account_05.dat")
|
||||
self.assertFalse(res["notifications"])
|
||||
self.assertEqual([d["journal_id"] for d in datas], [j1.id, j3.id])
|
||||
self.assertEqual(
|
||||
[x[:2] for x in self._lines(datas[0])],
|
||||
["01", "04", "05", "05", "05", "04", "05", "05", "07"],
|
||||
)
|
||||
|
||||
def test_multi_account_missing_journal(self):
|
||||
self._create_bank_journal("TCA1", IBAN_CA833[0])
|
||||
datas, res = self._split("cfonb120_lf_multi_account_05.dat")
|
||||
self.assertEqual(len(datas), 1)
|
||||
self.assertEqual(len(res["notifications"]), 1)
|
||||
self.assertEqual(res["notifications"][0]["type"], "error")
|
||||
self.assertIn("20000000203", res["notifications"][0]["message"])
|
||||
|
||||
def test_no_line_terminator_mixed_empty_and_full(self):
|
||||
"""Modelled on LCL: 4 accounts, no terminator, empty and full mixed."""
|
||||
journals = [
|
||||
self._create_bank_journal(f"TLC{i}", iban)
|
||||
for i, iban in enumerate(IBAN_LCL)
|
||||
]
|
||||
datas, res = self._split("cfonb120_no_eol_multi_account_mixed.dat")
|
||||
self.assertFalse(res["notifications"])
|
||||
self.assertEqual(
|
||||
[d["journal_id"] for d in datas], [journals[1].id, journals[3].id]
|
||||
)
|
||||
|
||||
def test_minimal_file_without_complement_lines(self):
|
||||
"""Modelled on Crédit Agricole 879: no 05 records, an account label in
|
||||
the free zone of 01 and dashes in 07, three empty statements before
|
||||
the only one with transactions."""
|
||||
journal = self._create_bank_journal("TCA9", IBAN_CA879[3])
|
||||
datas, res = self._split("cfonb120_lf_minimal_no_05.dat")
|
||||
self.assertFalse(res["notifications"])
|
||||
self.assertEqual(len(datas), 1)
|
||||
self.assertEqual(datas[0]["journal_id"], journal.id)
|
||||
self.assertEqual(
|
||||
[x[:2] for x in self._lines(datas[0])], ["01", "04", "04", "07"]
|
||||
)
|
||||
|
||||
def test_rich_complement_lines_are_kept(self):
|
||||
"""Modelled on CIC: every 05 qualifier stays with its statement."""
|
||||
self._create_bank_journal("TCIC", IBAN_CIC)
|
||||
datas, res = self._split("cfonb120_lf_rich_05.dat")
|
||||
self.assertFalse(res["notifications"])
|
||||
self.assertEqual(len(datas), 1)
|
||||
qualifiers = [x[45:48] for x in self._lines(datas[0]) if x[:2] == "05"]
|
||||
self.assertEqual(
|
||||
qualifiers,
|
||||
["NOM", "RIB", "IBE", "CBE", "LCC", "RCN", "REF", "NPY", "RUM", "LIB"],
|
||||
)
|
||||
|
||||
def test_bad_length(self):
|
||||
datas, res = self._split("cfonb120_bad_length.dat")
|
||||
self.assertEqual(datas, [])
|
||||
self.assertEqual(res["notifications"][0]["type"], "error")
|
||||
self.assertIn("120", res["notifications"][0]["message"])
|
||||
|
||||
def test_journal_in_other_currency(self):
|
||||
usd = self.env.ref("base.USD")
|
||||
self._create_bank_journal("TCMB", IBAN_CMB, currency=usd)
|
||||
datas, res = self._split("cfonb120_no_eol_single_account.dat")
|
||||
self.assertEqual(datas, [])
|
||||
self.assertEqual(res["notifications"][0]["type"], "error")
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestSplitCamt(EbicsTestCommon):
|
||||
def _split(self, sample):
|
||||
ebics_file = self._create_ebics_file(sample, self.ff_camt053)
|
||||
res = self._new_res()
|
||||
return ebics_file._split_camt(res), res
|
||||
|
||||
def _statements(self, st_data):
|
||||
root = etree.fromstring(base64.b64decode(st_data["data"]))
|
||||
ns = {"ns": root.nsmap[None]}
|
||||
return root.xpath("//ns:Stmt/ns:Acct/ns:Id//text()", namespaces=ns)
|
||||
|
||||
def test_single_statement_clav_fx(self):
|
||||
"""Modelled on UniCredit HVB: OPBD/CLBD/CLAV balances, FX entry."""
|
||||
journal = self._create_bank_journal("THVB", IBAN_HVB)
|
||||
datas, res = self._split("camt053_hvb_clav_fx.xml")
|
||||
self.assertFalse(res["notifications"])
|
||||
self.assertEqual(len(datas), 1)
|
||||
self.assertEqual(datas[0]["journal_id"], journal.id)
|
||||
self.assertEqual(datas[0]["acc_number"], IBAN_HVB)
|
||||
|
||||
def test_multi_account_is_split(self):
|
||||
j1 = self._create_bank_journal("TMU1", IBAN_MULTI[0])
|
||||
j2 = self._create_bank_journal("TMU2", IBAN_MULTI[1])
|
||||
datas, res = self._split("camt053_multi_account.xml")
|
||||
self.assertFalse(res["notifications"])
|
||||
self.assertEqual([d["journal_id"] for d in datas], [j1.id, j2.id])
|
||||
self.assertEqual(self._statements(datas[0]), [IBAN_MULTI[0]])
|
||||
self.assertEqual(self._statements(datas[1]), [IBAN_MULTI[1]])
|
||||
|
||||
def test_currency_suffix_is_stripped(self):
|
||||
"""Built from the code: some banks (e.g. Commerzbank) append the
|
||||
currency to the account number."""
|
||||
journal = self._create_bank_journal("TSUF", IBAN_SUFFIX)
|
||||
datas, res = self._split("camt053_currency_suffix.xml")
|
||||
self.assertFalse(res["notifications"])
|
||||
self.assertEqual(datas[0]["acc_number"], "0010000005")
|
||||
self.assertEqual(datas[0]["journal_id"], journal.id)
|
||||
|
||||
def test_statement_without_entries_is_skipped(self):
|
||||
self._create_bank_journal("TSPK", IBAN_SPK)
|
||||
datas, res = self._split("camt053_no_entries.xml")
|
||||
self.assertEqual(datas, [])
|
||||
self.assertFalse(res["notifications"])
|
||||
|
||||
def test_statement_without_account(self):
|
||||
datas, res = self._split("camt053_no_account.xml")
|
||||
self.assertEqual(datas, [])
|
||||
self.assertEqual(res["notifications"][0]["type"], "error")
|
||||
self.assertIn(
|
||||
"No bank account number found", res["notifications"][0]["message"]
|
||||
)
|
||||
|
||||
def test_invalid_xml(self):
|
||||
datas, res = self._split("invalid.xml")
|
||||
self.assertEqual(datas, [])
|
||||
self.assertEqual(res["notifications"][0]["type"], "error")
|
||||
self.assertIn("Invalid XML", res["notifications"][0]["message"])
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestLookupJournal(EbicsTestCommon):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.ebics_file = self._create_ebics_file(
|
||||
"camt053_sparkasse.xml", self.ff_camt053
|
||||
)
|
||||
self.res = self._new_res()
|
||||
|
||||
def test_unknown_currency(self):
|
||||
currency, journal = self.ebics_file._lookup_journal(
|
||||
self.res, "0010000002", "XYZ"
|
||||
)
|
||||
self.assertFalse(currency)
|
||||
self.assertFalse(journal)
|
||||
self.assertIn("XYZ", self.res["notifications"][0]["message"])
|
||||
|
||||
def test_no_journal(self):
|
||||
currency, journal = self.ebics_file._lookup_journal(
|
||||
self.res, "0010000002", "EUR"
|
||||
)
|
||||
self.assertEqual(currency, self.eur)
|
||||
self.assertFalse(journal)
|
||||
self.assertEqual(self.res["notifications"][0]["type"], "error")
|
||||
|
||||
def test_journal_currency_match(self):
|
||||
expected = self._create_bank_journal("TSPK", IBAN_SPK)
|
||||
currency, journal = self.ebics_file._lookup_journal(
|
||||
self.res, "0010000002", "eur"
|
||||
)
|
||||
self.assertEqual(currency, self.eur)
|
||||
self.assertEqual(journal, expected)
|
||||
self.assertFalse(self.res["notifications"])
|
||||
|
||||
def test_journal_without_currency_uses_company_currency(self):
|
||||
expected = self._create_bank_journal("TSPK", IBAN_SPK)
|
||||
expected.sudo().currency_id = False
|
||||
code = self.company.currency_id.name
|
||||
currency, journal = self.ebics_file._lookup_journal(
|
||||
self.res, "0010000002", code
|
||||
)
|
||||
self.assertEqual(currency, self.company.currency_id)
|
||||
self.assertEqual(journal, expected)
|
||||
@@ -0,0 +1,284 @@
|
||||
# Copyright 2026 Noviat.
|
||||
# License LGPL-3 or later (https://www.gnu.org/licenses/lgpl).
|
||||
|
||||
import base64
|
||||
import gc
|
||||
import itertools
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from unittest.mock import patch
|
||||
from urllib.error import URLError
|
||||
|
||||
from fintech.ebics import EbicsFunctionalError, EbicsKeyRing, EbicsTechnicalError
|
||||
|
||||
from odoo import Command, fields
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
from odoo.addons.account_ebics.models import ebics_userid as ebics_userid_module
|
||||
|
||||
from .common import EbicsTestCommon
|
||||
|
||||
# Throwaway passphrase for keys created in a temporary directory.
|
||||
WRONG_PHRASE = "wrong-passphrase"
|
||||
PUBLIC_BANK_KEYS = (
|
||||
"Public bank keys (simulated)\nX002 hash: 00 11 22\nE002 hash: 33 44 55\n"
|
||||
)
|
||||
|
||||
|
||||
def fake_init_client(hev=None, ini_error=None, hpb_error=None):
|
||||
"""Return a stand-in for fintech's EbicsClient covering the bank side of
|
||||
the key ceremony. Keys, certificates and the INI letter stay real."""
|
||||
|
||||
class FakeInitClient:
|
||||
calls = []
|
||||
|
||||
def __init__(self, bank, user, version=None):
|
||||
self.bank = bank
|
||||
|
||||
def HEV(self):
|
||||
self.calls.append("HEV")
|
||||
return hev if hev is not None else {"H004": "2.5", "H005": "3.0"}
|
||||
|
||||
def _order(self, name):
|
||||
self.calls.append(name)
|
||||
return self.bank._next_order_id("PARTNER1")
|
||||
|
||||
def INI(self):
|
||||
if ini_error:
|
||||
raise ini_error
|
||||
return self._order("INI")
|
||||
|
||||
def HIA(self):
|
||||
return self._order("HIA")
|
||||
|
||||
def HPB(self):
|
||||
if hpb_error:
|
||||
raise hpb_error
|
||||
self.calls.append("HPB")
|
||||
return PUBLIC_BANK_KEYS
|
||||
|
||||
return FakeInitClient
|
||||
|
||||
|
||||
class EbicsKeysCommon(EbicsTestCommon):
|
||||
"""Real keys in a temporary directory, simulated bank."""
|
||||
|
||||
_userid_seq = itertools.count(1)
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
keys_dir = tempfile.mkdtemp(prefix="ebics_keys_test_")
|
||||
cls.addClassCleanup(shutil.rmtree, keys_dir, ignore_errors=True)
|
||||
cls.ebics_config.write({"ebics_keys": keys_dir, "ebics_key_bitlength": 2048})
|
||||
cls.bank_fr = cls.env["res.bank"].create(
|
||||
{"name": "Banque Test", "country": cls.env.ref("base.fr").id}
|
||||
)
|
||||
cls.journal = cls._create_bank_journal("TKEY", "FR7630004000031234567890143")
|
||||
cls.journal.bank_account_id.sudo().bank_id = cls.bank_fr
|
||||
|
||||
def _new_userid(self, **vals):
|
||||
# A new name per test: fintech keeps open keyrings per file in-process.
|
||||
return self.env["ebics.userid"].create(
|
||||
{
|
||||
"name": f"KEYUSER{next(self._userid_seq)}",
|
||||
"ebics_config_id": self.ebics_config.id,
|
||||
"user_ids": [Command.set(self.env.user.ids)],
|
||||
"company_ids": [Command.set(self.company.ids)],
|
||||
"ebics_passphrase": "init-passphrase",
|
||||
**vals,
|
||||
}
|
||||
)
|
||||
|
||||
def _init_1(self, userid, client_class=None):
|
||||
client_class = client_class or fake_init_client()
|
||||
with patch.object(ebics_userid_module, "EbicsClient", client_class):
|
||||
userid.ebics_init_1()
|
||||
return client_class
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestKeyCeremony(EbicsKeysCommon):
|
||||
def test_full_ceremony(self):
|
||||
userid = self._new_userid(ebics_key_x509=True, ebics_key_x509_dn_cn="Test")
|
||||
client_class = self._init_1(userid)
|
||||
self.assertEqual(client_class.calls, ["HEV", "INI", "HIA"])
|
||||
self.assertEqual(userid.state, "init")
|
||||
self.assertTrue(os.path.isfile(userid.ebics_keys_fn))
|
||||
self.assertTrue(userid.ebics_keys_found)
|
||||
self.assertTrue(base64.b64decode(userid.ebics_ini_letter).startswith(b"%PDF"))
|
||||
today = fields.Date.today().isoformat()
|
||||
self.assertEqual(userid.ebics_ini_letter_fn, f"TESTHOST_ini_letter_{today}.pdf")
|
||||
|
||||
userid.ebics_init_2()
|
||||
self.assertEqual(userid.state, "get_bank_keys")
|
||||
|
||||
with patch.object(ebics_userid_module, "EbicsClient", client_class):
|
||||
userid.ebics_init_3()
|
||||
self.assertEqual(userid.state, "to_verify")
|
||||
self.assertEqual(
|
||||
base64.b64decode(userid.ebics_public_bank_keys).decode(), PUBLIC_BANK_KEYS
|
||||
)
|
||||
self.assertEqual(
|
||||
userid.ebics_public_bank_keys_fn, f"TESTHOST_public_bank_keys_{today}.txt"
|
||||
)
|
||||
|
||||
bank_class = ebics_userid_module.EbicsBank
|
||||
with patch.object(bank_class, "activate_keys") as activate_keys:
|
||||
userid.ebics_init_4()
|
||||
activate_keys.assert_called_once()
|
||||
self.assertEqual(userid.state, "active_keys")
|
||||
# stored passphrase is kept
|
||||
self.assertEqual(userid.ebics_passphrase, "init-passphrase")
|
||||
|
||||
def test_passphrase_not_stored_is_cleared(self):
|
||||
userid = self._new_userid(ebics_passphrase_store=False)
|
||||
self._init_1(userid)
|
||||
self.assertFalse(userid.ebics_passphrase)
|
||||
|
||||
def test_existing_keys_are_kept(self):
|
||||
"""A second init reuses the key file: reset tools must delete it."""
|
||||
userid = self._new_userid()
|
||||
self._init_1(userid)
|
||||
with open(userid.ebics_keys_fn, "rb") as f:
|
||||
keys = f.read()
|
||||
userid.set_to_draft()
|
||||
self._init_1(userid)
|
||||
with open(userid.ebics_keys_fn, "rb") as f:
|
||||
self.assertEqual(f.read(), keys)
|
||||
|
||||
def test_step_guards(self):
|
||||
userid = self._new_userid()
|
||||
userid.state = "init"
|
||||
with self.assertRaisesRegex(UserError, "draft"):
|
||||
userid.ebics_init_1()
|
||||
userid.state = "draft"
|
||||
for method in ("ebics_init_2", "ebics_init_3", "ebics_init_4"):
|
||||
with self.subTest(method=method), self.assertRaises(UserError):
|
||||
getattr(userid, method)()
|
||||
|
||||
def test_init_requires_passphrase(self):
|
||||
userid = self._new_userid(ebics_passphrase=False)
|
||||
with self.assertRaisesRegex(UserError, "passphrase"):
|
||||
userid.ebics_init_1()
|
||||
|
||||
def test_3skey_requires_certificate(self):
|
||||
userid = self._new_userid(swift_3skey=True)
|
||||
with self.assertRaisesRegex(UserError, "3SKey certificate"):
|
||||
userid.ebics_init_1()
|
||||
|
||||
def test_init_requires_a_journal_with_bank(self):
|
||||
self.ebics_config.journal_ids = False
|
||||
userid = self._new_userid()
|
||||
with self.assertRaisesRegex(UserError, "No bank defined"):
|
||||
self._init_1(userid)
|
||||
|
||||
def test_version_not_supported_by_bank(self):
|
||||
userid = self._new_userid()
|
||||
with self.assertRaisesRegex(UserError, "version mismatch"):
|
||||
self._init_1(userid, fake_init_client(hev={"H003": "2.4"}))
|
||||
self.assertEqual(userid.state, "draft")
|
||||
|
||||
def test_init_errors(self):
|
||||
cases = [
|
||||
(URLError("connection refused"), "urlopen error"),
|
||||
(
|
||||
EbicsFunctionalError("091002", "EBICS_INVALID_USER_OR_USER_STATE"),
|
||||
"091002",
|
||||
),
|
||||
(EbicsTechnicalError("061001", "EBICS_AUTHENTICATION_FAILED"), "061001"),
|
||||
]
|
||||
for error, expected in cases:
|
||||
with self.subTest(error=type(error).__name__):
|
||||
userid = self._new_userid()
|
||||
with (
|
||||
mute_logger(ebics_userid_module.__name__),
|
||||
self.assertRaisesRegex(UserError, expected),
|
||||
):
|
||||
self._init_1(userid, fake_init_client(ini_error=error))
|
||||
|
||||
def test_get_bank_keys_error(self):
|
||||
userid = self._new_userid()
|
||||
self._init_1(userid)
|
||||
userid.ebics_init_2()
|
||||
error = EbicsFunctionalError("091002", "EBICS_INVALID_USER_OR_USER_STATE")
|
||||
client_class = fake_init_client(hpb_error=error)
|
||||
with (
|
||||
patch.object(ebics_userid_module, "EbicsClient", client_class),
|
||||
self.assertRaisesRegex(UserError, "EBICS Functional Error"),
|
||||
):
|
||||
userid.ebics_init_3()
|
||||
|
||||
def test_h003_order_numbers(self):
|
||||
self.ebics_config.write({"ebics_version": "H003", "order_number": "A000"})
|
||||
userid = self._new_userid()
|
||||
self._init_1(userid, fake_init_client(hev={"H003": "2.4"}))
|
||||
# INI uses A000, HIA uses A001
|
||||
self.assertEqual(self.ebics_config.order_number, "A002")
|
||||
|
||||
def test_ini_letter_language(self):
|
||||
calls = []
|
||||
|
||||
class SpyUser(ebics_userid_module.EbicsUser):
|
||||
def create_ini_letter(self, bankname, lang=None, **kwargs):
|
||||
calls.append((bankname, lang))
|
||||
return b"%PDF-1.4 spy"
|
||||
|
||||
cases = [
|
||||
("base.fr", "fr_FR", "fr"),
|
||||
("base.de", "fr_FR", "de"),
|
||||
("base.be", "fr_FR", "fr"),
|
||||
("base.be", "en_US", "en"),
|
||||
]
|
||||
for country, user_lang, expected in cases:
|
||||
with self.subTest(country=country, user_lang=user_lang):
|
||||
lang = (
|
||||
self.env["res.lang"]
|
||||
.sudo()
|
||||
.with_context(active_test=False)
|
||||
.search([("code", "=", user_lang)])
|
||||
)
|
||||
lang.active = True
|
||||
self.bank_fr.country = self.env.ref(country)
|
||||
self.env.user.lang = user_lang
|
||||
userid = self._new_userid()
|
||||
with patch.object(ebics_userid_module, "EbicsUser", SpyUser):
|
||||
self._init_1(userid)
|
||||
self.assertEqual(calls[-1], ("Banque Test", expected))
|
||||
|
||||
def test_key_renewal_requires_draft_config(self):
|
||||
userid = self._new_userid()
|
||||
with self.assertRaisesRegex(UserError, "Key Renewal"):
|
||||
userid.set_to_get_bank_keys()
|
||||
self.ebics_config.set_to_draft()
|
||||
userid.set_to_get_bank_keys()
|
||||
self.assertEqual(userid.state, "get_bank_keys")
|
||||
|
||||
def test_h005_requires_x509(self):
|
||||
self.ebics_config.ebics_version = "H005"
|
||||
with self.assertRaisesRegex(UserError, "X.509"):
|
||||
self._new_userid(ebics_key_x509=False)
|
||||
|
||||
def test_stored_passphrase_is_checked(self):
|
||||
userid = self._new_userid()
|
||||
self._init_1(userid)
|
||||
gc.collect() # no other keyring instance: fintech raises ValueError
|
||||
userid.ebics_passphrase = WRONG_PHRASE
|
||||
with self.assertRaisesRegex(UserError, "Passphrase mismatch"):
|
||||
userid._onchange_ebics_passphrase_store()
|
||||
|
||||
def test_stored_passphrase_is_checked_with_open_keyring(self):
|
||||
"""In a worker the keyring may already be open with the right
|
||||
passphrase: fintech then raises RuntimeError."""
|
||||
userid = self._new_userid()
|
||||
self._init_1(userid)
|
||||
open_keyring = EbicsKeyRing(
|
||||
keys=userid.ebics_keys_fn, passphrase=userid.ebics_passphrase
|
||||
)
|
||||
userid.ebics_passphrase = WRONG_PHRASE
|
||||
with self.assertRaisesRegex(UserError, "Passphrase mismatch"):
|
||||
userid._onchange_ebics_passphrase_store()
|
||||
del open_keyring
|
||||
@@ -0,0 +1,245 @@
|
||||
# Copyright 2026 Noviat.
|
||||
# License LGPL-3 or later (https://www.gnu.org/licenses/lgpl).
|
||||
|
||||
import base64
|
||||
from datetime import date
|
||||
|
||||
from fintech.ebics import (
|
||||
EbicsFunctionalError,
|
||||
EbicsTechnicalError,
|
||||
EbicsVerificationError,
|
||||
)
|
||||
|
||||
from odoo.tests import tagged
|
||||
|
||||
from .common import EbicsTestCommon, FakeEbicsClient, read_sample
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestEbicsXferDownload(EbicsTestCommon):
|
||||
def test_line_terminator(self):
|
||||
xfer = self.env["ebics.xfer"]
|
||||
line_a, line_b = b"A" * 120, b"B" * 120
|
||||
self.assertEqual(
|
||||
xfer._handle_cfonb120(line_a + line_b), line_a + b"\n" + line_b + b"\n"
|
||||
)
|
||||
self.assertEqual(
|
||||
xfer._handle_cfonb120(line_a + b"\r\n" + line_b + b"\r\n"),
|
||||
line_a + b"\n" + line_b + b"\n",
|
||||
)
|
||||
self.assertEqual(
|
||||
xfer._handle_cfonb240(line_a + line_b), line_a + line_b + b"\n"
|
||||
)
|
||||
# Current behaviour: an incomplete trailing line is dropped.
|
||||
self.assertEqual(xfer._handle_cfonb120(line_a + b"C" * 10), line_a + b"\n")
|
||||
|
||||
def test_download_fdl_cfonb(self):
|
||||
data = read_sample("cfonb120_no_eol_single_account.dat")
|
||||
wizard = self._download_wizard(self.ff_cfonb120, date_to=date(2026, 10, 13))
|
||||
with self._fake_client(FakeEbicsClient(downloads={"FDL": data})) as client:
|
||||
action = wizard.ebics_download()
|
||||
self.assertEqual(action["context"]["err_cnt"], 0)
|
||||
ebics_file = self.env["ebics.file"].browse(action["context"]["ebics_file_ids"])
|
||||
self.assertEqual(ebics_file.name, "TESTHOST_PARTNER1_2026-10-13.cfonb120.dat")
|
||||
self.assertEqual(ebics_file.state, "draft")
|
||||
self.assertEqual(ebics_file.ebics_userid_id, self.ebics_userid)
|
||||
self.assertEqual(ebics_file.company_ids, self.ebics_config.company_ids)
|
||||
# FDL CFONB downloads get a line terminator every 120 characters
|
||||
stored = base64.b64decode(ebics_file.data)
|
||||
self.assertEqual(stored.count(b"\n"), 7)
|
||||
self.assertEqual(client.confirmed, [("TRANS0001", True)])
|
||||
self.assertIn("is available for further processing", wizard.note)
|
||||
|
||||
def test_download_btd_cfonb(self):
|
||||
data = read_sample("cfonb120_no_eol_single_account.dat")
|
||||
wizard = self._download_wizard(self.ff_btd_cfonb120, date_to=date(2026, 10, 13))
|
||||
with self._fake_client(FakeEbicsClient(downloads={"BTD": data})):
|
||||
action = wizard.ebics_download()
|
||||
ebics_file = self.env["ebics.file"].browse(action["context"]["ebics_file_ids"])
|
||||
self.assertEqual(len(ebics_file), 1)
|
||||
# BTD formats have no name, hence no line terminator handler.
|
||||
self.assertEqual(base64.b64decode(ebics_file.data), data)
|
||||
|
||||
def test_same_name_in_another_format(self):
|
||||
"""FDL and BTD CFONB formats share the file suffix: same file name,
|
||||
but a different format is not a duplicate."""
|
||||
data = read_sample("cfonb120_no_eol_single_account.dat")
|
||||
names = []
|
||||
for file_format, order_type in (
|
||||
(self.ff_cfonb120, "FDL"),
|
||||
(self.ff_btd_cfonb120, "BTD"),
|
||||
):
|
||||
wizard = self._download_wizard(file_format, date_to=date(2026, 10, 13))
|
||||
client = FakeEbicsClient(downloads={order_type: data})
|
||||
with self._fake_client(client):
|
||||
action = wizard.ebics_download()
|
||||
self.assertEqual(action["context"]["err_cnt"], 0)
|
||||
ebics_file = self.env["ebics.file"].browse(
|
||||
action["context"]["ebics_file_ids"]
|
||||
)
|
||||
names.append(ebics_file.name)
|
||||
self.assertEqual(names[0], names[1])
|
||||
|
||||
def test_download_same_file_twice(self):
|
||||
data = read_sample("cfonb120_no_eol_single_account.dat")
|
||||
for expected_errors in (0, 1):
|
||||
wizard = self._download_wizard(self.ff_cfonb120, date_to=date(2026, 10, 13))
|
||||
with self._fake_client(FakeEbicsClient(downloads={"FDL": data})) as client:
|
||||
action = wizard.ebics_download()
|
||||
self.assertEqual(action["context"]["err_cnt"], expected_errors)
|
||||
self.assertIn("has already been downloaded", wizard.note)
|
||||
self.assertEqual(client.confirmed, [("TRANS0001", False)])
|
||||
|
||||
def test_download_several_documents(self):
|
||||
ptk = self.env["ebics.file.format"].create(
|
||||
{"name": "PTK", "type": "down", "order_type": "PTK", "suffix": "txt"}
|
||||
)
|
||||
data = read_sample("ptk_protocol.txt")
|
||||
wizard = self._download_wizard(ptk)
|
||||
client = FakeEbicsClient(downloads={"PTK": {"doc1": data, "doc2": data}})
|
||||
with self._fake_client(client):
|
||||
action = wizard.ebics_download()
|
||||
ebics_files = self.env["ebics.file"].browse(action["context"]["ebics_file_ids"])
|
||||
self.assertEqual(
|
||||
sorted(ebics_files.mapped("name")),
|
||||
["TESTHOST_PARTNER1_doc1.txt", "TESTHOST_PARTNER1_doc2.txt"],
|
||||
)
|
||||
# no processing method: the files are done right away
|
||||
self.assertEqual(set(ebics_files.mapped("state")), {"done"})
|
||||
|
||||
def test_mark_as_downloaded(self):
|
||||
data = read_sample("cfonb120_no_eol_single_account.dat")
|
||||
wizard = self._download_wizard(self.ff_cfonb120).with_context(
|
||||
ebics_mark_as_downloaded=True
|
||||
)
|
||||
with self._fake_client(FakeEbicsClient(downloads={"FDL": data})) as client:
|
||||
action = wizard.ebics_download()
|
||||
self.assertFalse(action["context"]["ebics_file_ids"])
|
||||
self.assertEqual(client.confirmed, [("TRANS0001", True)])
|
||||
|
||||
def test_download_errors(self):
|
||||
cases = [
|
||||
(
|
||||
EbicsFunctionalError("091005", "EBICS_NO_DOWNLOAD_DATA_AVAILABLE"),
|
||||
"EBICS Functional Error",
|
||||
"EBICS_NO_DOWNLOAD_DATA_AVAILABLE (code: 091005)",
|
||||
),
|
||||
(
|
||||
EbicsTechnicalError("061002", "EBICS_INVALID_REQUEST"),
|
||||
"EBICS Technical Error",
|
||||
"EBICS_INVALID_REQUEST (code: 061002)",
|
||||
),
|
||||
(
|
||||
EbicsVerificationError(),
|
||||
"EBICS Verification Error",
|
||||
"could not be verified",
|
||||
),
|
||||
(RuntimeError("socket closed"), "Unknown Error", "socket closed"),
|
||||
]
|
||||
for error, title, detail in cases:
|
||||
with self.subTest(error=type(error).__name__):
|
||||
wizard = self._download_wizard(self.ff_cfonb120)
|
||||
with self._fake_client(FakeEbicsClient(error=error)) as client:
|
||||
action = wizard.ebics_download()
|
||||
self.assertEqual(action["context"]["err_cnt"], 1)
|
||||
self.assertFalse(action["context"]["ebics_file_ids"])
|
||||
self.assertIn(title, wizard.note)
|
||||
self.assertIn(detail, wizard.note)
|
||||
self.assertEqual(client.confirmed, [("TRANS0001", False)])
|
||||
|
||||
def test_client_setup_failure(self):
|
||||
wizard = self._download_wizard(self.ff_cfonb120)
|
||||
with self._fake_client(False):
|
||||
action = wizard.ebics_download()
|
||||
self.assertEqual(action["context"]["err_cnt"], 1)
|
||||
self.assertIn("client setup failed", wizard.note)
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestEbicsXferUpload(EbicsTestCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.ff_btu = cls.env["ebics.file.format"].create(
|
||||
{
|
||||
"type": "up",
|
||||
"order_type": "BTU",
|
||||
"btf_service": "SCT",
|
||||
"btf_message": "pain.001",
|
||||
"suffix": "xml",
|
||||
}
|
||||
)
|
||||
cls.ff_ful = cls.env.ref("account_ebics.ebics_ff_LCR")
|
||||
bank = cls.env["res.bank"].create(
|
||||
{"name": "Test Bank FR", "country": cls.env.ref("base.fr").id}
|
||||
)
|
||||
journal = cls._create_bank_journal("TUPL", "FR7630004000031234567890143")
|
||||
journal.bank_account_id.sudo().bank_id = bank
|
||||
|
||||
def test_upload_btu(self):
|
||||
wizard = self._upload_wizard(self.ff_btu, test_mode=True)
|
||||
with self._fake_client(FakeEbicsClient(order_id="N042")) as client:
|
||||
ebics_file = wizard._ebics_upload()
|
||||
self.assertEqual(ebics_file.name, "pay.xml")
|
||||
self.assertEqual(ebics_file.state, "done")
|
||||
self.assertEqual(ebics_file.pain_state, "uploaded")
|
||||
self.assertEqual(ebics_file.ebics_order_number, "N042")
|
||||
self.assertEqual(client.uploads, [("BTU", b"<Document/>", {"TEST": "TRUE"})])
|
||||
self.assertIn("OrderID N042", wizard.note)
|
||||
# H004: the order number is managed by the bank
|
||||
self.assertFalse(self.ebics_config.order_number)
|
||||
|
||||
def test_upload_same_name_twice(self):
|
||||
names = []
|
||||
for _i in range(3):
|
||||
wizard = self._upload_wizard(self.ff_btu)
|
||||
with self._fake_client(FakeEbicsClient()):
|
||||
names.append(wizard._ebics_upload().name)
|
||||
# Pinned, probably unintended: _update_ef_vals appends each counter to
|
||||
# the previous candidate (pay.xml_1_2) instead of pay.xml_2.
|
||||
self.assertEqual(names, ["pay.xml", "pay.xml_1", "pay.xml_1_2"])
|
||||
|
||||
def test_upload_ful_h003_order_number(self):
|
||||
self.ebics_config.write({"ebics_version": "H003", "order_number": "A00Z"})
|
||||
wizard = self._upload_wizard(self.ff_ful, fname="lcr.txt")
|
||||
with self._fake_client(FakeEbicsClient(order_id="A00Z")) as client:
|
||||
ebics_file = wizard._ebics_upload()
|
||||
self.assertEqual(ebics_file.ebics_order_number, "A00Z")
|
||||
self.assertEqual(client.uploads[0][0], "FUL")
|
||||
self.assertEqual(client.uploads[0][2], {"country": "FR"})
|
||||
self.assertEqual(self.ebics_config.order_number, "A010")
|
||||
|
||||
def test_upload_h003_failure_still_increments(self):
|
||||
"""Pinned behaviour: the order number moves on even if the upload failed."""
|
||||
self.ebics_config.write({"ebics_version": "H003", "order_number": "B000"})
|
||||
wizard = self._upload_wizard(self.ff_ful, fname="lcr.txt")
|
||||
error = EbicsFunctionalError("091112", "EBICS_INVALID_ORDER_PARAMS")
|
||||
with self._fake_client(FakeEbicsClient(error=error)):
|
||||
ebics_file = wizard._ebics_upload()
|
||||
self.assertFalse(ebics_file)
|
||||
self.assertEqual(self.ebics_config.order_number, "B001")
|
||||
self.assertIn("EBICS Functional Error", wizard.note)
|
||||
self.assertIn("EBICS_INVALID_ORDER_PARAMS (code: 091112)", wizard.note)
|
||||
|
||||
def test_upload_errors(self):
|
||||
cases = [
|
||||
(EbicsTechnicalError("061001", "EBICS_AUTHENTICATION_FAILED"), "Technical"),
|
||||
(EbicsVerificationError(), "Verification"),
|
||||
(RuntimeError("socket closed"), "Unknown Error"),
|
||||
]
|
||||
for error, expected in cases:
|
||||
with self.subTest(error=type(error).__name__):
|
||||
wizard = self._upload_wizard(self.ff_btu)
|
||||
with self._fake_client(FakeEbicsClient(error=error)):
|
||||
self.assertFalse(wizard._ebics_upload())
|
||||
self.assertIn(expected, wizard.note)
|
||||
|
||||
def test_upload_onchange_selects_format(self):
|
||||
"""A single upload format on the config is selected automatically."""
|
||||
self.ebics_config.ebics_file_format_ids = self.ff_btu
|
||||
wizard = self.env["ebics.xfer"].new(
|
||||
{"ebics_config_id": self.ebics_config.id, "upload_fname": "pay.xml"}
|
||||
)
|
||||
wizard._onchange_ebics_config_id()
|
||||
self.assertEqual(wizard.format_id, self.ff_btu)
|
||||
self.assertEqual(wizard.ebics_userid_id, self.ebics_userid)
|
||||
@@ -0,0 +1,115 @@
|
||||
# Copyright 2026 Noviat.
|
||||
# License LGPL-3 or later (https://www.gnu.org/licenses/lgpl).
|
||||
|
||||
import base64
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.account_ebics.wizards import ebics_xfer as ebics_xfer_module
|
||||
|
||||
from .test_ebics_userid import EbicsKeysCommon
|
||||
|
||||
# Throwaway passphrase for keys created in a temporary directory.
|
||||
WRONG_PHRASE = "wrong-passphrase"
|
||||
|
||||
|
||||
def capturing_client(captured, init_error=None):
|
||||
"""Stand-in for fintech's EbicsClient that records the real EbicsBank and
|
||||
EbicsUser built by ebics.xfer._setup_client."""
|
||||
|
||||
class CapturingClient:
|
||||
def __init__(self, bank, user, version=None):
|
||||
if init_error:
|
||||
raise init_error
|
||||
captured.update(bank=bank, user=user, version=version)
|
||||
self.bank = bank
|
||||
|
||||
def FUL(self, filetype, data, **kwargs):
|
||||
# As fintech does on H003: the OrderID comes from the bank object.
|
||||
return self.bank._next_order_id(captured["user"].partnerid)
|
||||
|
||||
return CapturingClient
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestSetupClient(EbicsKeysCommon):
|
||||
"""ebics.xfer._setup_client with real keys; only the transport is faked."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.userid = self._new_userid()
|
||||
self._init_1(self.userid)
|
||||
self.userid.state = "active_keys"
|
||||
self.ff_ful = self.env.ref("account_ebics.ebics_ff_LCR")
|
||||
|
||||
def _wizard(self, file_format=None, **vals):
|
||||
return self.env["ebics.xfer"].create(
|
||||
{
|
||||
"ebics_config_id": self.ebics_config.id,
|
||||
"ebics_userid_id": self.userid.id,
|
||||
"format_id": (file_format or self.ff_ful).id,
|
||||
"upload_data": base64.b64encode(b"LCR DATA"),
|
||||
"upload_fname": "lcr.txt",
|
||||
"note": "",
|
||||
**vals,
|
||||
}
|
||||
)
|
||||
|
||||
def _setup(self, wizard, init_error=None):
|
||||
captured = {}
|
||||
client_class = capturing_client(captured, init_error=init_error)
|
||||
with patch.object(ebics_xfer_module, "EbicsClient", client_class):
|
||||
client = wizard._setup_client()
|
||||
return client, captured
|
||||
|
||||
def test_client_from_config_and_keys(self):
|
||||
client, captured = self._setup(self._wizard())
|
||||
self.assertTrue(client)
|
||||
self.assertEqual(captured["version"], "H004")
|
||||
self.assertEqual(captured["bank"].hostid, "TESTHOST")
|
||||
self.assertEqual(captured["bank"].url, "https://ebics.example.invalid/ebics")
|
||||
self.assertEqual(captured["user"].partnerid, "PARTNER1")
|
||||
self.assertEqual(captured["user"].userid, self.userid.name)
|
||||
|
||||
def test_signature_class(self):
|
||||
cases = [
|
||||
# (userid default, file format override, expected transport_only)
|
||||
("T", False, True),
|
||||
("E", False, False),
|
||||
("T", "E", False),
|
||||
("E", "T", True),
|
||||
]
|
||||
for user_class, format_class, expected in cases:
|
||||
with self.subTest(user=user_class, file_format=format_class):
|
||||
self.userid.signature_class = user_class
|
||||
self.ff_ful.signature_class = format_class
|
||||
_client, captured = self._setup(self._wizard())
|
||||
self.assertEqual(captured["user"].transport_only, expected)
|
||||
|
||||
def test_h003_order_number_is_given_to_the_bank(self):
|
||||
self.ebics_config.write({"ebics_version": "H003", "order_number": "B00Z"})
|
||||
wizard = self._wizard()
|
||||
captured = {}
|
||||
with patch.object(ebics_xfer_module, "EbicsClient", capturing_client(captured)):
|
||||
ebics_file = wizard._ebics_upload()
|
||||
self.assertEqual(ebics_file.ebics_order_number, "B00Z")
|
||||
self.assertEqual(self.ebics_config.order_number, "B010")
|
||||
|
||||
def test_wrong_passphrase(self):
|
||||
wizard = self._wizard(ebics_passphrase=WRONG_PHRASE)
|
||||
with self.assertRaisesRegex(UserError, "Error while accessing the EBICS Keys"):
|
||||
self._setup(wizard)
|
||||
|
||||
def test_missing_keys_directory(self):
|
||||
self.ebics_config.ebics_keys = "/nonexistent/ebics_keys"
|
||||
with self.assertRaisesRegex(UserError, "is not available"):
|
||||
self._setup(self._wizard())
|
||||
|
||||
def test_client_creation_error(self):
|
||||
wizard = self._wizard()
|
||||
client, _captured = self._setup(wizard, init_error=RuntimeError("bad url"))
|
||||
self.assertFalse(client)
|
||||
self.assertIn("Unknown Error", wizard.note)
|
||||
self.assertIn("bad url", wizard.note)
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
{
|
||||
"name": "EBICS - Scheduled File Import",
|
||||
"version": "18.0.1.0.2",
|
||||
"version": "18.0.1.0.3",
|
||||
"license": "AGPL-3",
|
||||
"author": "Noviat",
|
||||
"website": "https://www.noviat.com/",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from . import test_ebics_batch_import
|
||||
@@ -0,0 +1,95 @@
|
||||
# Copyright 2026 Noviat.
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from fintech.ebics import EbicsTechnicalError
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.account_ebics.tests.common import (
|
||||
EbicsTestCommon,
|
||||
FakeEbicsClient,
|
||||
read_sample,
|
||||
)
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestEbicsBatchImport(EbicsTestCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
# A format without processing method: files are stored as done.
|
||||
cls.ff_ptk = cls.env["ebics.file.format"].create(
|
||||
{"name": "PTK", "type": "down", "order_type": "PTK", "suffix": "txt"}
|
||||
)
|
||||
cls.ebics_config.ebics_file_format_ids = cls.ff_ptk
|
||||
cls.log_model = cls.env["ebics.batch.log"]
|
||||
|
||||
def _run(self, client, **kwargs):
|
||||
with self._fake_client(client):
|
||||
self.log_model._batch_import(
|
||||
ebics_config_ids=self.ebics_config.ids, **kwargs
|
||||
)
|
||||
return self.log_model.search([], order="id desc", limit=1)
|
||||
|
||||
def test_import(self):
|
||||
client = FakeEbicsClient(downloads={"PTK": read_sample("ptk_protocol.txt")})
|
||||
log = self._run(client, date_from="2026-10-12", date_to="2026-10-13")
|
||||
self.assertEqual(log.state, "done")
|
||||
self.assertEqual(log.ebics_config_ids, self.ebics_config)
|
||||
self.assertEqual(log.file_count, 1)
|
||||
self.assertEqual(log.file_ids.name, "TESTHOST_PARTNER1_2026-10-13.txt")
|
||||
self.assertEqual(log.log_ids.state, "done")
|
||||
self.assertEqual(log.log_ids.error_count, 0)
|
||||
|
||||
def test_import_prefers_transport_signature_user(self):
|
||||
"""Downloads need no signature right: a class T user is preferred."""
|
||||
self.ebics_userid.signature_class = "E"
|
||||
transport_userid = self.ebics_userid.copy(
|
||||
{"name": "USER2", "signature_class": "T", "state": "active_keys"}
|
||||
)
|
||||
client = FakeEbicsClient(downloads={"PTK": read_sample("ptk_protocol.txt")})
|
||||
log = self._run(client, date_to="2026-10-13")
|
||||
self.assertEqual(log.file_ids.ebics_userid_id, transport_userid)
|
||||
|
||||
def test_import_transfer_error(self):
|
||||
error = EbicsTechnicalError("061001", "EBICS_AUTHENTICATION_FAILED")
|
||||
log = self._run(FakeEbicsClient(error=error))
|
||||
self.assertEqual(log.state, "error")
|
||||
self.assertFalse(log.file_ids)
|
||||
self.assertEqual(log.log_ids.error_count, 1)
|
||||
self.assertIn("EBICS_AUTHENTICATION_FAILED", log.log_ids.note)
|
||||
|
||||
def test_import_draft_config(self):
|
||||
self.ebics_config.set_to_draft()
|
||||
log = self._run(FakeEbicsClient())
|
||||
self.assertEqual(log.state, "error")
|
||||
self.assertIn("Please set state to 'Confirm'", log.log_ids.note)
|
||||
|
||||
def test_import_without_stored_passphrase(self):
|
||||
self.ebics_userid.ebics_passphrase_store = False
|
||||
log = self._run(FakeEbicsClient())
|
||||
self.assertEqual(log.state, "error")
|
||||
self.assertIn("No EBICS UserID with stored passphrase", log.log_ids.note)
|
||||
|
||||
def test_draft_files_are_processed_and_reprocessed(self):
|
||||
ebics_file = self._create_ebics_file("camt053_sparkasse.xml", self.ff_camt053)
|
||||
log = self.log_model.create({"file_ids": [(6, 0, ebics_file.ids)]})
|
||||
self.assertTrue(log.has_draft_files)
|
||||
file_class = type(ebics_file)
|
||||
with patch.object(file_class, "process", autospec=True) as process:
|
||||
log.reprocess()
|
||||
process.assert_called_once_with(ebics_file)
|
||||
# the file is still draft since processing was mocked
|
||||
self.assertEqual(log.state, "draft")
|
||||
|
||||
def test_unlink_only_draft_logs(self):
|
||||
log = self.log_model.create({})
|
||||
log.button_done()
|
||||
with self.assertRaises(UserError):
|
||||
log.unlink()
|
||||
log.button_draft()
|
||||
log.unlink()
|
||||
self.assertFalse(log.exists())
|
||||
@@ -4,7 +4,7 @@
|
||||
{
|
||||
"name": "EBICS - Batch Payments (Odoo Enterprise)",
|
||||
"summary": "Send an Odoo Enterprise batch payment to the bank over EBICS",
|
||||
"version": "18.0.1.3.0",
|
||||
"version": "18.0.1.3.1",
|
||||
"license": "LGPL-3",
|
||||
"author": "Noviat",
|
||||
"website": "https://www.noviat.com/",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from . import test_batch_payment_upload
|
||||
@@ -0,0 +1,77 @@
|
||||
# Copyright 2026 Noviat.
|
||||
# License LGPL-3 or later (https://www.gnu.org/licenses/lgpl).
|
||||
|
||||
import base64
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.account_ebics.tests.common import EbicsTestCommon, FakeEbicsClient
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestBatchPaymentUpload(EbicsTestCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.journal = cls._create_bank_journal("TBP", "FR7630004000031234567890143")
|
||||
cls.ff_cct = cls.env.ref("account_ebics.ebics_ff_CCT")
|
||||
cls.method = cls.env.ref("account.account_payment_method_manual_out")
|
||||
|
||||
def _batch(self, state="sent"):
|
||||
batch = self.env["account.batch.payment"].create(
|
||||
{
|
||||
"journal_id": self.journal.id,
|
||||
"batch_type": "outbound",
|
||||
"payment_method_id": self.method.id,
|
||||
"export_file": base64.b64encode(b"<Document>pain.001</Document>"),
|
||||
"export_filename": "batch.xml",
|
||||
}
|
||||
)
|
||||
# Without payments the batch stays in draft: set the state directly.
|
||||
batch.state = state
|
||||
return batch
|
||||
|
||||
def _generating_files(self):
|
||||
batch_class = type(self.env["account.batch.payment"])
|
||||
return patch.object(
|
||||
batch_class, "_get_methods_generating_files", return_value=["manual"]
|
||||
)
|
||||
|
||||
def test_hide_ebics_upload(self):
|
||||
with self._generating_files():
|
||||
self.assertFalse(self._batch().hide_ebics_upload)
|
||||
self.assertTrue(self._batch(state="draft").hide_ebics_upload)
|
||||
batch = self._batch()
|
||||
batch.ebics_file_id = self.env["ebics.file"].create(
|
||||
{"name": "batch.xml", "date": "2026-10-13", "pain_state": "uploaded"}
|
||||
)
|
||||
self.assertTrue(batch.hide_ebics_upload)
|
||||
# no file generation for this payment method
|
||||
self.assertTrue(self._batch().hide_ebics_upload)
|
||||
|
||||
def test_upload_requires_ebics_config(self):
|
||||
self.ebics_config.journal_ids = False
|
||||
with self.assertRaises(UserError):
|
||||
self._batch().ebics_upload()
|
||||
|
||||
def test_upload_format_from_payment_method(self):
|
||||
self.method.ebics_file_format_id = self.ff_cct
|
||||
action = self._batch().ebics_upload()
|
||||
wizard = self.env["ebics.xfer"].browse(action["res_id"])
|
||||
self.assertEqual(wizard.format_id, self.ff_cct)
|
||||
self.assertEqual(wizard.upload_fname, "batch.xml")
|
||||
|
||||
def test_upload(self):
|
||||
self.method.ebics_file_format_id = self.ff_cct
|
||||
batch = self._batch()
|
||||
action = batch.ebics_upload()
|
||||
wizard = self.env["ebics.xfer"].browse(action["res_id"])
|
||||
with self._fake_client(FakeEbicsClient(order_id="N088")) as client:
|
||||
wizard.with_context(**action["context"]).ebics_upload()
|
||||
self.assertEqual(client.uploads[0][1], b"<Document>pain.001</Document>")
|
||||
self.assertEqual(batch.ebics_order_number, "N088")
|
||||
self.assertEqual(batch.ebics_pain_state, "uploaded")
|
||||
self.assertIn(f"Batch Payment: {batch.name}", batch.ebics_file_id.note)
|
||||
self.assertTrue(any("N088" in msg.body for msg in batch.message_ids))
|
||||
@@ -0,0 +1,15 @@
|
||||
=========================================
|
||||
EBICS - Enterprise Statement Import Tests
|
||||
=========================================
|
||||
|
||||
**Test-only module. Do not install it on a production database.**
|
||||
|
||||
It carries no code and no data. It exists so that continuous integration
|
||||
installs the Odoo Enterprise CAMT parser (``account_bank_statement_import_camt``)
|
||||
next to ``account_ebics`` and runs the statement import tests through the
|
||||
Enterprise import. It is excluded from the Community test job.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Developed and maintained by `Noviat <https://www.noviat.com>`_.
|
||||
Whitespace-only changes.
@@ -0,0 +1,17 @@
|
||||
# Copyright 2026 Noviat.
|
||||
# License LGPL-3 or later (https://www.gnu.org/licenses/lgpl).
|
||||
|
||||
{
|
||||
"name": "EBICS - Enterprise Statement Import Tests",
|
||||
"summary": "Test-only module, do not install: runs the EBICS statement "
|
||||
"import tests against the Odoo Enterprise CAMT parser",
|
||||
"version": "18.0.1.0.0",
|
||||
"license": "LGPL-3",
|
||||
"author": "Noviat",
|
||||
"website": "https://www.noviat.com/",
|
||||
"category": "Hidden",
|
||||
"depends": [
|
||||
"account_ebics",
|
||||
"account_bank_statement_import_camt",
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
[build-system]
|
||||
requires = ["whool"]
|
||||
build-backend = "whool.buildapi"
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1 @@
|
||||
from . import test_oe_statement_import
|
||||
@@ -0,0 +1,66 @@
|
||||
# Copyright 2026 Noviat.
|
||||
# License LGPL-3 or later (https://www.gnu.org/licenses/lgpl).
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.account_ebics.tests.common import (
|
||||
IBAN_HVB,
|
||||
IBAN_MULTI,
|
||||
EbicsTestCommon,
|
||||
)
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestOeStatementImport(EbicsTestCommon):
|
||||
"""End to end through the Odoo Enterprise camt import: EBICS file ->
|
||||
split per account -> ir.attachment -> journal._import_bank_statement."""
|
||||
|
||||
def _enterprise_parser_only(self):
|
||||
"""Both camt parsers are installed in the Enterprise CI job and
|
||||
_process_camt053 prefers the OCA one: hide it."""
|
||||
file_class = type(self.env["ebics.file"])
|
||||
check_import_module = file_class._check_import_module
|
||||
|
||||
def check(record, module, raise_if_not_found=True):
|
||||
if module == "account_statement_import_camt":
|
||||
return False
|
||||
return check_import_module(record, module, raise_if_not_found)
|
||||
|
||||
return patch.object(file_class, "_check_import_module", check)
|
||||
|
||||
def _process(self, sample, name=None):
|
||||
ebics_file = self._create_ebics_file(sample, self.ff_camt053, name=name)
|
||||
with self._enterprise_parser_only():
|
||||
ebics_file.process()
|
||||
self.assertEqual(ebics_file.state, "done")
|
||||
return ebics_file
|
||||
|
||||
def test_camt_statement(self):
|
||||
journal = self._create_bank_journal("THVB", IBAN_HVB)
|
||||
ebics_file = self._process("camt053_hvb_clav_fx.xml")
|
||||
statement = ebics_file.bank_statement_ids
|
||||
self.assertEqual(statement.journal_id, journal)
|
||||
self.assertEqual(statement.balance_start, 5000.0)
|
||||
self.assertEqual(statement.balance_end_real, 3992.5)
|
||||
self.assertEqual(sorted(statement.line_ids.mapped("amount")), [-857.5, -150.0])
|
||||
self.assertEqual(statement.import_format, "camt053")
|
||||
self.assertIn("1 bank statement has been imported", ebics_file.note_process)
|
||||
|
||||
def test_camt_multi_account(self):
|
||||
j1 = self._create_bank_journal("TMU1", IBAN_MULTI[0])
|
||||
j2 = self._create_bank_journal("TMU2", IBAN_MULTI[1])
|
||||
ebics_file = self._process("camt053_multi_account.xml")
|
||||
self.assertEqual(ebics_file.bank_statement_ids.journal_id, j1 | j2)
|
||||
self.assertIn("2 bank statements have been imported", ebics_file.note_process)
|
||||
|
||||
def test_reimport_is_reported(self):
|
||||
self._create_bank_journal("THVB", IBAN_HVB)
|
||||
sample = "camt053_hvb_clav_fx.xml"
|
||||
first = self._process(sample, name="first.xml")
|
||||
self.assertEqual(len(first.bank_statement_ids), 1)
|
||||
second = self._process(sample, name="second.xml")
|
||||
self.assertFalse(second.bank_statement_ids)
|
||||
self.assertIn("has not been processed", second.note_process)
|
||||
self.assertIn("0010000001", second.note_process)
|
||||
@@ -4,7 +4,7 @@
|
||||
{
|
||||
"name": "EBICS - Payment Orders (OCA)",
|
||||
"summary": "Send an OCA payment order to the bank over EBICS",
|
||||
"version": "18.0.1.3.0",
|
||||
"version": "18.0.1.3.1",
|
||||
"license": "LGPL-3",
|
||||
"author": "Noviat",
|
||||
"website": "https://www.noviat.com/",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from . import test_payment_order_upload
|
||||
@@ -0,0 +1,95 @@
|
||||
# Copyright 2026 Noviat.
|
||||
# License LGPL-3 or later (https://www.gnu.org/licenses/lgpl).
|
||||
|
||||
import base64
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.account_ebics.tests.common import EbicsTestCommon, FakeEbicsClient
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestPaymentOrderUpload(EbicsTestCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env.user.groups_id |= cls.env.ref(
|
||||
"account_payment_order.group_account_payment"
|
||||
)
|
||||
cls.journal = cls._create_bank_journal("TPO", "FR7630004000031234567890143")
|
||||
cls.ff_cct = cls.env.ref("account_ebics.ebics_ff_CCT")
|
||||
cls.mode = cls.env["account.payment.mode"].create(
|
||||
{
|
||||
"name": "EBICS credit transfer",
|
||||
"company_id": cls.company.id,
|
||||
"bank_account_link": "fixed",
|
||||
"fixed_journal_id": cls.journal.id,
|
||||
"payment_method_id": cls.env.ref(
|
||||
"account.account_payment_method_manual_out"
|
||||
).id,
|
||||
"ebics_format_id": cls.ff_cct.id,
|
||||
}
|
||||
)
|
||||
|
||||
def _order(self, state="generated", with_file=True):
|
||||
order = self.env["account.payment.order"].create(
|
||||
{
|
||||
"payment_type": "outbound",
|
||||
"payment_mode_id": self.mode.id,
|
||||
"journal_id": self.journal.id,
|
||||
}
|
||||
)
|
||||
order.state = state
|
||||
if with_file:
|
||||
self._attach(order, "pain001.xml")
|
||||
return order
|
||||
|
||||
def _attach(self, order, name):
|
||||
return self.env["ir.attachment"].create(
|
||||
{
|
||||
"name": name,
|
||||
"res_model": order._name,
|
||||
"res_id": order.id,
|
||||
"datas": base64.b64encode(b"<Document>pain.001</Document>"),
|
||||
}
|
||||
)
|
||||
|
||||
def test_hide_ebics_upload(self):
|
||||
self.assertTrue(self._order(state="draft").hide_ebics_upload)
|
||||
self.assertFalse(self._order().hide_ebics_upload)
|
||||
self.ebics_config.journal_ids = False
|
||||
self.assertTrue(self._order().hide_ebics_upload)
|
||||
|
||||
def test_upload_requires_format_on_mode(self):
|
||||
self.mode.ebics_format_id = False
|
||||
with self.assertRaisesRegex(UserError, "EBICS File Format"):
|
||||
self._order().ebics_upload()
|
||||
|
||||
def test_upload_requires_one_file(self):
|
||||
with self.assertRaisesRegex(UserError, "doesn't contain attachments"):
|
||||
self._order(with_file=False).ebics_upload()
|
||||
order = self._order()
|
||||
self._attach(order, "pain001_old.xml")
|
||||
with self.assertRaisesRegex(UserError, "multiple attachments"):
|
||||
order.ebics_upload()
|
||||
|
||||
def test_upload(self):
|
||||
order = self._order()
|
||||
# the form button passes the active model, as the web client does
|
||||
action = order.with_context(
|
||||
active_model=order._name, active_id=order.id
|
||||
).ebics_upload()
|
||||
wizard = self.env["ebics.xfer"].browse(action["res_id"])
|
||||
self.assertEqual(wizard.ebics_config_id, self.ebics_config)
|
||||
self.assertEqual(wizard.ebics_userid_id, self.ebics_userid)
|
||||
self.assertEqual(wizard.format_id, self.ff_cct)
|
||||
self.assertEqual(wizard.upload_fname, "pain001.xml")
|
||||
with self._fake_client(FakeEbicsClient(order_id="N077")) as client:
|
||||
wizard.with_context(**action["context"]).ebics_upload()
|
||||
self.assertEqual(client.uploads[0][1], b"<Document>pain.001</Document>")
|
||||
self.assertEqual(order.state, "uploaded")
|
||||
self.assertEqual(order.ebics_order_number, "N077")
|
||||
self.assertEqual(order.ebics_file_id.pain_state, "uploaded")
|
||||
self.assertIn(f"Payment Order: {order.name}", order.ebics_file_id.note)
|
||||
self.assertTrue(any("N077" in msg.body for msg in order.message_ids))
|
||||
@@ -0,0 +1,16 @@
|
||||
==============================
|
||||
EBICS - Statement Import Tests
|
||||
==============================
|
||||
|
||||
**Test-only module. Do not install it on a production database.**
|
||||
|
||||
It carries no code and no data. It exists so that continuous integration
|
||||
installs the OCA statement parsers (``account_statement_import_camt`` and
|
||||
``account_statement_import_fr_cfonb``) next to ``account_ebics`` and runs the
|
||||
end-to-end statement import tests: EBICS download, split per account, bank
|
||||
statement creation.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Developed and maintained by `Noviat <https://www.noviat.com>`_.
|
||||
Whitespace-only changes.
@@ -0,0 +1,18 @@
|
||||
# Copyright 2026 Noviat.
|
||||
# License LGPL-3 or later (https://www.gnu.org/licenses/lgpl).
|
||||
|
||||
{
|
||||
"name": "EBICS - Statement Import Tests",
|
||||
"summary": "Test-only module, do not install: runs the EBICS statement "
|
||||
"import tests against the OCA CAMT and CFONB parsers",
|
||||
"version": "18.0.1.0.0",
|
||||
"license": "LGPL-3",
|
||||
"author": "Noviat",
|
||||
"website": "https://www.noviat.com/",
|
||||
"category": "Hidden",
|
||||
"depends": [
|
||||
"account_ebics_oca_statement_import",
|
||||
"account_statement_import_camt",
|
||||
"account_statement_import_fr_cfonb",
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
[build-system]
|
||||
requires = ["whool"]
|
||||
build-backend = "whool.buildapi"
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -0,0 +1 @@
|
||||
from . import test_statement_import
|
||||
@@ -0,0 +1,145 @@
|
||||
# Copyright 2026 Noviat.
|
||||
# License LGPL-3 or later (https://www.gnu.org/licenses/lgpl).
|
||||
|
||||
from datetime import date
|
||||
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.account_ebics.tests.common import (
|
||||
IBAN_CA833,
|
||||
IBAN_CA879,
|
||||
IBAN_CIC,
|
||||
IBAN_CMB,
|
||||
IBAN_HVB,
|
||||
IBAN_LCL,
|
||||
IBAN_MULTI,
|
||||
IBAN_SUFFIX,
|
||||
EbicsTestCommon,
|
||||
FakeEbicsClient,
|
||||
read_sample,
|
||||
)
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestStatementImport(EbicsTestCommon):
|
||||
"""End to end: EBICS file -> split per account -> OCA parser -> statements.
|
||||
|
||||
These assertions hold whether or not the account_ebics_oca_statement_import
|
||||
glue is installed (it does not exist on 19.0)."""
|
||||
|
||||
def _process(self, sample, file_format, name=None):
|
||||
ebics_file = self._create_ebics_file(sample, file_format, name=name)
|
||||
ebics_file.process()
|
||||
self.assertEqual(ebics_file.state, "done")
|
||||
return ebics_file
|
||||
|
||||
def test_cfonb_multi_account(self):
|
||||
j1 = self._create_bank_journal("TCA1", IBAN_CA833[0])
|
||||
j3 = self._create_bank_journal("TCA3", IBAN_CA833[2])
|
||||
ebics_file = self._process("cfonb120_lf_multi_account_05.dat", self.ff_cfonb120)
|
||||
statements = ebics_file.bank_statement_ids.sorted("journal_id")
|
||||
self.assertEqual(statements.journal_id, j1 | j3)
|
||||
st1 = statements.filtered(lambda s: s.journal_id == j1)
|
||||
self.assertEqual(st1.balance_start, 8400.0)
|
||||
self.assertAlmostEqual(st1.balance_end_real, 8907.45)
|
||||
self.assertEqual(sorted(st1.line_ids.mapped("amount")), [-132.55, 640.0])
|
||||
self.assertIn("FACT 99812", " ".join(st1.line_ids.mapped("payment_ref")))
|
||||
self.assertEqual(set(statements.mapped("import_format")), {"cfonb120"})
|
||||
self.assertIn("2 bank statements have been imported", ebics_file.note_process)
|
||||
|
||||
def test_cfonb_minimal_without_complement_lines(self):
|
||||
journal = self._create_bank_journal("TCA9", IBAN_CA879[3])
|
||||
ebics_file = self._process("cfonb120_lf_minimal_no_05.dat", self.ff_cfonb120)
|
||||
statement = ebics_file.bank_statement_ids
|
||||
self.assertEqual(statement.journal_id, journal)
|
||||
self.assertEqual(len(statement.line_ids), 2)
|
||||
self.assertEqual(statement.balance_end_real, 5290.0)
|
||||
|
||||
def test_cfonb_no_line_terminator(self):
|
||||
journals = [
|
||||
self._create_bank_journal(f"TLC{i}", iban)
|
||||
for i, iban in enumerate(IBAN_LCL)
|
||||
]
|
||||
ebics_file = self._process(
|
||||
"cfonb120_no_eol_multi_account_mixed.dat", self.ff_cfonb120
|
||||
)
|
||||
self.assertEqual(
|
||||
ebics_file.bank_statement_ids.journal_id, journals[1] | journals[3]
|
||||
)
|
||||
|
||||
def test_cfonb_complement_lines_in_label(self):
|
||||
self._create_bank_journal("TCIC", IBAN_CIC)
|
||||
ebics_file = self._process("cfonb120_lf_rich_05.dat", self.ff_cfonb120)
|
||||
refs = " ".join(ebics_file.bank_statement_ids.line_ids.mapped("payment_ref"))
|
||||
self.assertIn("FACTURE 7731", refs)
|
||||
self.assertIn("SALE", refs)
|
||||
|
||||
def test_cfonb_missing_journal_is_reported(self):
|
||||
self._create_bank_journal("TCA1", IBAN_CA833[0])
|
||||
ebics_file = self._process("cfonb120_lf_multi_account_05.dat", self.ff_cfonb120)
|
||||
self.assertEqual(len(ebics_file.bank_statement_ids), 1)
|
||||
self.assertIn("No financial journal found", ebics_file.note_process)
|
||||
self.assertIn("20000000203", ebics_file.note_process)
|
||||
|
||||
def test_cfonb_empty_file(self):
|
||||
self._create_bank_journal("TCMB", IBAN_CMB)
|
||||
ebics_file = self._process("cfonb120_no_eol_empty_only.dat", self.ff_cfonb120)
|
||||
self.assertFalse(ebics_file.bank_statement_ids)
|
||||
self.assertIn("doesn't contain any transaction", ebics_file.note_process)
|
||||
|
||||
def test_camt_fx_entry(self):
|
||||
journal = self._create_bank_journal("THVB", IBAN_HVB)
|
||||
ebics_file = self._process("camt053_hvb_clav_fx.xml", self.ff_camt053)
|
||||
statement = ebics_file.bank_statement_ids
|
||||
self.assertEqual(statement.journal_id, journal)
|
||||
self.assertEqual(statement.balance_start, 5000.0)
|
||||
self.assertEqual(statement.balance_end_real, 3992.5)
|
||||
self.assertEqual(sorted(statement.line_ids.mapped("amount")), [-857.5, -150.0])
|
||||
self.assertEqual(statement.import_format, "camt053")
|
||||
|
||||
def test_camt_multi_account(self):
|
||||
j1 = self._create_bank_journal("TMU1", IBAN_MULTI[0])
|
||||
j2 = self._create_bank_journal("TMU2", IBAN_MULTI[1])
|
||||
ebics_file = self._process("camt053_multi_account.xml", self.ff_camt053)
|
||||
self.assertEqual(ebics_file.bank_statement_ids.journal_id, j1 | j2)
|
||||
|
||||
def test_download_then_process(self):
|
||||
journal = self._create_bank_journal("TCMB", IBAN_CMB)
|
||||
data = read_sample("cfonb120_no_eol_single_account.dat")
|
||||
wizard = self._download_wizard(self.ff_cfonb120, date_to=date(2026, 10, 13))
|
||||
with self._fake_client(FakeEbicsClient(downloads={"FDL": data})):
|
||||
action = wizard.ebics_download()
|
||||
ebics_file = self.env["ebics.file"].browse(action["context"]["ebics_file_ids"])
|
||||
ebics_file.process()
|
||||
self.assertEqual(ebics_file.bank_statement_ids.journal_id, journal)
|
||||
self.assertEqual(len(ebics_file.bank_statement_ids.line_ids), 2)
|
||||
self.assertEqual(
|
||||
ebics_file.bank_statement_ids.ebics_file_id,
|
||||
ebics_file,
|
||||
)
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestStatementImportOcaGlue(EbicsTestCommon):
|
||||
"""Behaviour added by account_ebics_oca_statement_import (18.0 only).
|
||||
|
||||
Drop this class when forward-porting to a version without that module."""
|
||||
|
||||
def test_reimport_is_reported(self):
|
||||
self._create_bank_journal("TCMB", IBAN_CMB)
|
||||
sample = "cfonb120_no_eol_single_account.dat"
|
||||
first = self._create_ebics_file(sample, self.ff_cfonb120, name="first.dat")
|
||||
first.process()
|
||||
self.assertEqual(len(first.bank_statement_ids), 1)
|
||||
second = self._create_ebics_file(sample, self.ff_cfonb120, name="second.dat")
|
||||
second.process()
|
||||
self.assertFalse(second.bank_statement_ids)
|
||||
self.assertIn("has already been imported", second.note_process)
|
||||
|
||||
def test_currency_suffix_on_account(self):
|
||||
journal = self._create_bank_journal("TSUF", IBAN_SUFFIX)
|
||||
ebics_file = self._create_ebics_file(
|
||||
"camt053_currency_suffix.xml", self.ff_camt053
|
||||
)
|
||||
ebics_file.process()
|
||||
self.assertEqual(ebics_file.bank_statement_ids.journal_id, journal)
|
||||
@@ -1,3 +1,5 @@
|
||||
odoo-addon-module_change_auto_install
|
||||
openupgradelib
|
||||
lxml_html_clean
|
||||
odoo-addon-account_statement_import_camt
|
||||
odoo-addon-account_statement_import_fr_cfonb
|
||||
Reference in new issue
Block a user