Merge pull request #3 from Noviat/16.0

Syncing from upstream Noviat/account_ebics (16.0)
This commit is contained in:
braintec 2023-08-06 01:09:42 +02:00 committed by GitHub
commit cc75cb0df3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 263 additions and 336 deletions

View File

@ -27,8 +27,6 @@ and fintech 7.2.7 or higher for EBICS 3.0.
SWIFT 3SKey support requires fintech 6.4 or higher. SWIFT 3SKey support requires fintech 6.4 or higher.
| |
We also recommend to consider the installation of the following modules: We also recommend to consider the installation of the following modules:
@ -71,9 +69,17 @@ We also recommend to consider the installation of the following modules:
- account_ebics_payment_order - account_ebics_payment_order
Recommended if you are using the OCA account_payment_order module. Required if you are using the OCA account_payment_order module.
Cf. https://github.com/Noviat/account_ebics and https://github.com/OCA/bank-payment Cf. https://github.com/OCA/bank-payment
|
- account_ebics_oca_statement_import
Required if you are using the OCA Bank Statement import modules.
https://github.com/OCA/bank-statement-import
| |
@ -93,27 +99,6 @@ We also recommend to consider the installation of the following modules:
| |
- account_statement_import_helper
Required if you are processing bank statements with local bank account numbers (e.g. french CFONB files)
and using import parsers based upon the OCA account_statement_import module.
The import helper will match the local bank account number with the IBAN number specified on the Odoo Financial journal.
Cf. https://github.com/Noviat/noviat-apps
|
- account_bank_statement_import_helper
Required if you are processing bank statements with local bank account numbers
and using import parsers based upon the Odoo Enterprise account_bank_statement_import module.
The import helper will match the local bank account number with the IBAN number specified on the Odoo Financial journal.
Cf. https://github.com/Noviat/noviat-apps
|
Fintech license Fintech license
--------------- ---------------
@ -130,13 +115,6 @@ The name of the licensee.
The keycode of the licensed version. The keycode of the licensed version.
- fintech_register_users
The licensed EBICS user ids. It must be a string or a list of user ids.
You should NOT specify this parameter if your license is subsciption
based (with monthly recurring billing).
| |
| Example: | Example:
| |
@ -146,7 +124,6 @@ based (with monthly recurring billing).
; fintech ; fintech
fintech_register_name = MyCompany fintech_register_name = MyCompany
fintech_register_keycode = AB1CD-E2FG-3H-IJ4K-5L fintech_register_keycode = AB1CD-E2FG-3H-IJ4K-5L
fintech_register_users = USER1, USER2
| |

View File

@ -3,7 +3,7 @@
{ {
"name": "EBICS banking protocol", "name": "EBICS banking protocol",
"version": "16.0.1.3.1", "version": "16.0.1.4.0",
"license": "LGPL-3", "license": "LGPL-3",
"author": "Noviat", "author": "Noviat",
"website": "https://www.noviat.com", "website": "https://www.noviat.com",

View File

@ -8,3 +8,4 @@ class AccountBankStatement(models.Model):
_inherit = "account.bank.statement" _inherit = "account.bank.statement"
ebics_file_id = fields.Many2one(comodel_name="ebics.file", string="EBICS Data File") ebics_file_id = fields.Many2one(comodel_name="ebics.file", string="EBICS Data File")
import_format = fields.Char(readonly=True)

View File

@ -91,14 +91,6 @@ class EbicsConfig(models.Model):
"between customer and financial institution. " "between customer and financial institution. "
"The human user also can authorise orders.", "The human user also can authorise orders.",
) )
ebics_files = fields.Char(
string="EBICS Files Root",
required=True,
readonly=True,
states={"draft": [("readonly", False)]},
default=lambda self: self._default_ebics_files(),
help="Root Directory for EBICS File Transfer Folders.",
)
# We store the EBICS keys in a separate directory in the file system. # We store the EBICS keys in a separate directory in the file system.
# This directory requires special protection to reduce fraude. # This directory requires special protection to reduce fraude.
ebics_keys = fields.Char( ebics_keys = fields.Char(
@ -253,14 +245,3 @@ class EbicsConfig(models.Model):
) )
% dirname % dirname
) )
def _check_ebics_files(self):
dirname = self.ebics_files or ""
if not os.path.exists(dirname):
raise UserError(
_(
"EBICS Files Root Directory %s is not available."
"\nPlease contact your system administrator."
)
% dirname
)

View File

@ -11,12 +11,13 @@ from lxml import etree
from odoo import _, fields, models from odoo import _, fields, models
from odoo.exceptions import UserError from odoo.exceptions import UserError
from odoo.tools.safe_eval import safe_eval
from odoo.addons.base.models.res_bank import sanitize_account_number from odoo.addons.base.models.res_bank import sanitize_account_number
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
DUP_CHECK_FORMATS = ["cfonb120", "camt053"]
class EbicsFile(models.Model): class EbicsFile(models.Model):
_name = "ebics.file" _name = "ebics.file"
@ -177,31 +178,95 @@ class EbicsFile(models.Model):
return False return False
return True return True
def _process_download_result(self, res): def _lookup_journal(self, res, acc_number, currency_code):
currency = self.env["res.currency"].search(
[("name", "=ilike", currency_code)], limit=1
)
journal = self.env["account.journal"]
if not currency:
message = _("Currency %(cc)s not found.", cc=currency_code)
res["notifications"].append({"type": "error", "message": message})
return (currency, journal)
journals = self.env["account.journal"].search(
[
("type", "=", "bank"),
(
"bank_account_id.sanitized_acc_number",
"ilike",
acc_number,
),
]
)
if not journals:
message = _(
"No financial journal found for Account Number %(nbr)s, "
"Currency %(cc)s",
nbr=acc_number,
cc=currency_code,
)
res["notifications"].append({"type": "error", "message": message})
return (currency, journal)
for jrnl in journals:
journal_currency = jrnl.currency_id or jrnl.company_id.currency_id
if journal_currency != currency:
continue
else:
journal = jrnl
break
if not journal:
message = _(
"No financial journal found for Account Number %(nbr)s, "
"Currency %(cc)s",
nbr=acc_number,
cc=currency_code,
)
res["notifications"].append({"type": "error", "message": message})
return (currency, journal)
def _process_download_result(self, res, file_format=None):
"""
We perform a duplicate statement check after the creation of the bank
statements since we rely on Odoo Enterprise or OCA modules for the
bank statement creation.
From a development standpoint (code creation/maintenance) a check after
creation is the easiest way.
"""
statement_ids = res["statement_ids"] statement_ids = res["statement_ids"]
notifications = res["notifications"] notifications = res["notifications"]
statements = self.env["account.bank.statement"].sudo().browse(statement_ids) statements = self.env["account.bank.statement"].sudo().browse(statement_ids)
st_cnt = len(statement_ids) if statements:
statements.write({"import_format": file_format})
statements = self._statement_duplicate_check(res, statements)
st_cnt = len(statements)
warning_cnt = error_cnt = 0 warning_cnt = error_cnt = 0
if notifications: if notifications:
errors = []
warnings = []
for notif in notifications: for notif in notifications:
if notif["type"] == "error": if notif["type"] == "error":
error_cnt += 1 error_cnt += 1
parts = [notif[k] for k in notif if k in ("message", "details")]
errors.append("\n".join(parts))
elif notif["type"] == "warning": elif notif["type"] == "warning":
warning_cnt += 1 warning_cnt += 1
parts = [notif[k] for k in notif if k in ("message", "details")] parts = [notif[k] for k in notif if k in ("message", "details")]
self.note_process += "\n".join(parts) warnings.append("\n".join(parts))
self.note_process += "\n\n"
self.note_process += "\n" self.note_process += _("Process file %(fn)s results:", fn=self.name)
if error_cnt: if error_cnt:
self.note_process += ( self.note_process += "\n\n" + _("Errors") + ":\n"
_("Number of errors detected during import: %s") % error_cnt self.note_process += "\n".join(errors)
) self.note_process += "\n\n"
self.note_process += "\n" self.note_process += _("Number of errors: %(nr)s", nr=error_cnt)
if warning_cnt: if warning_cnt:
self.note_process += ( self.note_process += "\n\n" + _("Warnings") + ":\n"
_("Number of warnings detected during import: %s") % warning_cnt self.note_process += "\n".join(warnings)
) self.note_process += "\n\n"
self.note_process += _("Number of warnings: %(nr)s", nr=warning_cnt)
self.note_process += "\n"
if st_cnt: if st_cnt:
self.note_process += "\n\n" self.note_process += "\n\n"
self.note_process += _( self.note_process += _(
@ -211,16 +276,17 @@ class EbicsFile(models.Model):
) )
self.note_process += "\n" self.note_process += "\n"
for statement in statements: for statement in statements:
self.note_process += ("\n%s, %s (%s)") % ( self.note_process += "\n" + _(
statement.date, "Statement %(st)s dated %(date)s (Company: %(cpy)s)",
statement.name, st=statement.name,
statement.company_id.name, date=statement.date,
cpy=statement.company_id.name,
) )
if statement_ids: if statements:
self.sudo().bank_statement_ids = [(4, x) for x in statement_ids] self.sudo().bank_statement_ids = [(4, x) for x in statements.ids]
company_ids = self.sudo().bank_statement_ids.mapped("company_id").ids company_ids = self.sudo().bank_statement_ids.mapped("company_id").ids
self.company_ids = [(6, 0, company_ids)] self.company_ids = [(6, 0, company_ids)]
ctx = dict(self.env.context, statement_ids=statement_ids) ctx = dict(self.env.context, statement_ids=statements.ids)
module = __name__.split("addons.")[1].split(".")[0] module = __name__.split("addons.")[1].split(".")[0]
result_view = self.env.ref("%s.ebics_file_view_form_result" % module) result_view = self.env.ref("%s.ebics_file_view_form_result" % module)
return { return {
@ -235,104 +301,47 @@ class EbicsFile(models.Model):
"type": "ir.actions.act_window", "type": "ir.actions.act_window",
} }
def _process_cfonb120(self): def _statement_duplicate_check(self, res, statements):
""" """
Disable this code while waiting on OCA cfonb release for 16.0 This check is required for import modules that do not
set the 'unique_import_id' on the statement lines.
E.g. OCA camt import
""" """
# pylint: disable=W0101 to_unlink = self.env["account.bank.statement"]
raise NotImplementedError for statement in statements.filtered(
lambda r: r.import_format in DUP_CHECK_FORMATS
):
dup = self.env["account.bank.statement"].search_count(
[
("id", "!=", statement.id),
("name", "=", statement.name),
("company_id", "=", statement.company_id.id),
("date", "=", statement.date),
("import_format", "=", statement.import_format),
]
)
if dup:
message = _(
"Statement %(st_name)s dated %(date)s has already been imported.",
st_name=statement.name,
date=statement.date,
)
res["notifications"].append({"type": "warning", "message": message})
to_unlink += statement
res["statement_ids"] = [
x for x in res["statement_ids"] if x not in to_unlink.ids
]
statements -= to_unlink
to_unlink.unlink()
return statements
def _process_cfonb120(self):
import_module = "account_statement_import_fr_cfonb" import_module = "account_statement_import_fr_cfonb"
self._check_import_module(import_module) self._check_import_module(import_module)
wiz_model = "account.statement.import" res = {"statement_ids": [], "notifications": []}
data_file = base64.b64decode(self.data) st_datas = self._split_cfonb(res)
lines = data_file.split(b"\n") self._process_bank_statement_oca(res, st_datas)
wiz_vals_list = [] return self._process_download_result(res, file_format="cfonb120")
st_lines = b""
transactions = False
for line in lines:
rec_type = line[0:2]
acc_number = line[21:32]
st_lines += line + b"\n"
if rec_type == b"04":
transactions = True
if rec_type == b"07":
if transactions:
fn = "_".join([acc_number.decode(), self.name])
wiz_vals_list.append(
{
"statement_filename": fn,
"statement_file": base64.b64encode(st_lines),
}
)
st_lines = b""
transactions = False
result_action = self.env["ir.actions.act_window"]._for_xml_id(
"account.action_bank_statement_tree"
)
result_action["context"] = safe_eval(result_action["context"])
statement_ids = []
notifications = []
for i, wiz_vals in enumerate(wiz_vals_list, start=1):
result = {
"statement_ids": [],
"notifications": [],
}
statement_filename = wiz_vals["statement_filename"]
wiz = (
self.env[wiz_model]
.with_context(active_model="ebics.file")
.create(wiz_vals)
)
try:
with self.env.cr.savepoint():
file_data = base64.b64decode(wiz_vals["statement_file"])
msg_hdr = _(
"{} : Import failed for statement number %(index)s, filename %(fn)s:\n",
index=i,
fn=statement_filename,
)
wiz.import_single_file(file_data, result)
if not result["statement_ids"]:
message = msg_hdr.format(_("Warning"))
message += _(
"You have already imported this file, or this file "
"only contains already imported transactions."
)
notifications += [
{
"type": "warning",
"message": message,
}
]
else:
statement_ids.extend(result["statement_ids"])
notifications.extend(result["notifications"])
except UserError as e:
message = msg_hdr.format(_("Error"))
message += "".join(e.args)
notifications += [
{
"type": "error",
"message": message,
}
]
except Exception:
tb = "".join(format_exception(*exc_info()))
message = msg_hdr.format(_("Error"))
message += tb
notifications += [
{
"type": "error",
"message": message,
}
]
result_action["context"]["notifications"] = notifications
result_action["domain"] = [("id", "in", statement_ids)]
return self._process_result_action(result_action)
def _unlink_cfonb120(self): def _unlink_cfonb120(self):
""" """
@ -340,10 +349,45 @@ class EbicsFile(models.Model):
EBICS data file and its related bank statements. EBICS data file and its related bank statements.
""" """
def _split_cfonb(self, res):
"""
Split CFONB file received via EBICS per statement.
Statements without transactions are removed.
"""
datas = []
file_data = base64.b64decode(self.data)
lines = file_data.split(b"\n")
st_lines = b""
transactions = False
for line in lines:
rec_type = line[0:2]
currency_code = line[16:19].decode()
acc_number = line[21:32].decode()
st_lines += line + b"\n"
if rec_type == b"04":
transactions = True
if rec_type == b"07":
if transactions:
currency, journal = self._lookup_journal(
res, acc_number, currency_code
)
if currency and journal:
datas.append(
{
"acc_number": acc_number,
"journal_id": journal.id,
"company_id": journal.company_id.id,
"data": base64.b64encode(st_lines),
}
)
st_lines = b""
transactions = False
return datas
def _process_camt052(self): def _process_camt052(self):
import_module = "account_statement_import_camt" import_module = "account_statement_import_camt"
self._check_import_module(import_module) self._check_import_module(import_module)
return self._process_camt053(self) return self._process_camt053(file_format="camt052")
def _unlink_camt052(self): def _unlink_camt052(self):
""" """
@ -354,7 +398,7 @@ class EbicsFile(models.Model):
def _process_camt054(self): def _process_camt054(self):
import_module = "account_statement_import_camt" import_module = "account_statement_import_camt"
self._check_import_module(import_module) self._check_import_module(import_module)
return self._process_camt053(self) return self._process_camt053(file_format="camt054")
def _unlink_camt054(self): def _unlink_camt054(self):
""" """
@ -362,7 +406,7 @@ class EbicsFile(models.Model):
EBICS data file and its related bank statements. EBICS data file and its related bank statements.
""" """
def _process_camt053(self): def _process_camt053(self, file_format=None):
""" """
The Odoo standard statement import is based on manual selection The Odoo standard statement import is based on manual selection
of a financial journal before importing the electronic statement file. of a financial journal before importing the electronic statement file.
@ -394,28 +438,26 @@ class EbicsFile(models.Model):
res = {"statement_ids": [], "notifications": []} res = {"statement_ids": [], "notifications": []}
st_datas = self._split_camt(res) st_datas = self._split_camt(res)
if author == "oca": if author == "oca":
self._process_camt053_oca(res, st_datas) self._process_bank_statement_oca(res, st_datas)
else: else:
self._process_camt053_oe(res, st_datas) self._process_bank_statement_oe(res, st_datas)
return self._process_download_result(res) file_format = file_format or "camt053"
return self._process_download_result(res, file_format=file_format)
def _process_camt053_oca(self, res, st_datas): def _process_bank_statement_oca(self, res, st_datas):
msg_hdr = _("{} : Import failed for file %(fn)s:\n", fn=self.name)
for st_data in st_datas: for st_data in st_datas:
try: try:
with self.env.cr.savepoint(): with self.env.cr.savepoint():
self._create_statement_camt053_oca(res, st_data) self._create_bank_statement_oca(res, st_data)
except UserError as e: except UserError as e:
message = msg_hdr.format(_("Error")) res["notifications"].append(
message += "".join(e.args) {"type": "error", "message": "".join(e.args)}
res["notifications"].append({"type": "error", "message": message}) )
except Exception: except Exception:
tb = "".join(format_exception(*exc_info())) tb = "".join(format_exception(*exc_info()))
message = msg_hdr.format(_("Error")) res["notifications"].append({"type": "error", "message": tb})
message += tb
res["notifications"].append({"type": "error", "message": message})
def _create_statement_camt053_oca(self, res, st_data): def _create_bank_statement_oca(self, res, st_data):
wiz = ( wiz = (
self.env["account.statement.import"] self.env["account.statement.import"]
.with_company(st_data["company_id"]) .with_company(st_data["company_id"])
@ -424,27 +466,28 @@ class EbicsFile(models.Model):
) )
wiz.import_single_file(base64.b64decode(st_data["data"]), res) wiz.import_single_file(base64.b64decode(st_data["data"]), res)
def _process_camt053_oe(self, res, st_datas): def _process_bank_statement_oe(self, res, st_datas):
""" """
We execute a cr.commit() after every statement import since we get a We execute a cr.commit() after every statement import since we get a
'savepoint does not exist' error when using 'with self.env.cr.savepoint()'. 'savepoint does not exist' error when using 'with self.env.cr.savepoint()'.
""" """
msg_hdr = _("{} : Import failed for file %(fn)s:\n", fn=self.name)
for st_data in st_datas: for st_data in st_datas:
try: try:
self._create_statement_camt053_oe(res, st_data) self._create_bank_statement_oe(res, st_data)
self.env.cr.commit() # pylint: disable=E8102 self.env.cr.commit() # pylint: disable=E8102
except UserError as e: except UserError as e:
message = msg_hdr.format(_("Error")) msg = "".join(e.args)
message += "".join(e.args) msg += "\n"
res["notifications"].append({"type": "error", "message": message}) msg += _(
"Statement for Account Number %(nr)s has not been processed.",
nr=st_data["acc_number"],
)
res["notifications"].append({"type": "error", "message": msg})
except Exception: except Exception:
tb = "".join(format_exception(*exc_info())) tb = "".join(format_exception(*exc_info()))
message = msg_hdr.format(_("Error")) res["notifications"].append({"type": "error", "message": tb})
message += tb
res["notifications"].append({"type": "error", "message": message})
def _create_statement_camt053_oe(self, res, st_data): def _create_bank_statement_oe(self, res, st_data):
attachment = ( attachment = (
self.env["ir.attachment"] self.env["ir.attachment"]
.with_company(st_data["company_id"]) .with_company(st_data["company_id"])
@ -486,20 +529,14 @@ class EbicsFile(models.Model):
Statements without transactions are removed. Statements without transactions are removed.
""" """
datas = [] datas = []
msg_hdr = _("{} : Import failed for file %(fn)s:\n", fn=self.name)
file_data = base64.b64decode(self.data) file_data = base64.b64decode(self.data)
root = etree.fromstring(file_data, parser=etree.XMLParser(recover=True)) root = etree.fromstring(file_data, parser=etree.XMLParser(recover=True))
if root is None: if root is None:
message = msg_hdr.format(_("Error")) message = _("Invalid XML file.")
message += _("Invalid XML file.")
res["notifications"].append({"type": "error", "message": message}) res["notifications"].append({"type": "error", "message": message})
ns = {k or "ns": v for k, v in root.nsmap.items()} ns = {k or "ns": v for k, v in root.nsmap.items()}
for i, stmt in enumerate(root[0].findall("ns:Stmt", ns), start=1): stmts = root[0].findall("ns:Stmt", ns)
msg_hdr = _( for i, stmt in enumerate(stmts):
"{} : Import failed for statement number %(index)s, filename %(fn)s:\n",
index=i,
fn=self.name,
)
acc_number = sanitize_account_number( acc_number = sanitize_account_number(
stmt.xpath( stmt.xpath(
"ns:Acct/ns:Id/ns:IBAN/text() | ns:Acct/ns:Id/ns:Othr/ns:Id/text()", "ns:Acct/ns:Id/ns:IBAN/text() | ns:Acct/ns:Id/ns:Othr/ns:Id/text()",
@ -507,72 +544,34 @@ class EbicsFile(models.Model):
)[0] )[0]
) )
if not acc_number: if not acc_number:
message = msg_hdr.format(_("Error")) message = _("No bank account number found.")
message += _("No bank account number found.")
res["notifications"].append({"type": "error", "message": message}) res["notifications"].append({"type": "error", "message": message})
continue continue
currency_code = stmt.xpath( currency_code = stmt.xpath(
"ns:Acct/ns:Ccy/text() | ns:Bal/ns:Amt/@Ccy", namespaces=ns "ns:Acct/ns:Ccy/text() | ns:Bal/ns:Amt/@Ccy", namespaces=ns
)[0] )[0]
currency = self.env["res.currency"].search(
[("name", "=ilike", currency_code)], limit=1
)
if not currency:
message = msg_hdr.format(_("Error"))
message += _("Currency %(cc)s not found.", cc=currency_code)
res["notifications"].append({"type": "error", "message": message})
continue
journal = self.env["account.journal"].search(
[
("type", "=", "bank"),
(
"bank_account_id.sanitized_acc_number",
"ilike",
acc_number,
),
]
)
if not journal:
message = msg_hdr.format(_("Error"))
message += _(
"No financial journal found for Account Number %(nbr)s, "
"Currency %(cc)s",
nbr=acc_number,
cc=currency_code,
)
res["notifications"].append({"type": "error", "message": message})
continue
journal_currency = journal.currency_id or journal.company_id.currency_id
if journal_currency != currency:
message = msg_hdr.format(_("Error"))
message += _(
"No financial journal found for Account Number %(nbr)s, "
"Currency %(cc)s",
nbr=acc_number,
cc=currency_code,
)
res["notifications"].append({"type": "error", "message": message})
continue
root_new = deepcopy(root) root_new = deepcopy(root)
entries = False entries = False
for j, el in enumerate(root_new[0].findall("ns:Stmt", ns), start=1): for j, el in enumerate(root_new[0].findall("ns:Stmt", ns)):
if j != i: if j != i:
el.getparent().remove(el) el.getparent().remove(el)
else: else:
entries = el.findall("ns:Ntry", ns) entries = el.findall("ns:Ntry", ns)
if not entries: if not entries:
continue continue
else:
datas.append( currency, journal = self._lookup_journal(res, acc_number, currency_code)
{ if not (currency and journal):
"acc_number": acc_number, continue
"journal_id": journal.id, datas.append(
"company_id": journal.company_id.id, {
"data": base64.b64encode(etree.tostring(root_new)), "acc_number": acc_number,
} "journal_id": journal.id,
) "company_id": journal.company_id.id,
"data": base64.b64encode(etree.tostring(root_new)),
}
)
return datas return datas

View File

@ -129,7 +129,7 @@ class EbicsUserID(models.Model):
"by means of the SWIFT 3SKey token.", "by means of the SWIFT 3SKey token.",
) )
swift_3skey_certificate = fields.Binary(string="3SKey Certficate") swift_3skey_certificate = fields.Binary(string="3SKey Certficate")
swift_3skey_certificate_fn = fields.Char(string="EBICS certificate name") swift_3skey_certificate_fn = fields.Char(string="3SKey Certificate Filename")
# X.509 Distinguished Name attributes used to # X.509 Distinguished Name attributes used to
# create self-signed X.509 certificates # create self-signed X.509 certificates
ebics_key_x509 = fields.Boolean( ebics_key_x509 = fields.Boolean(
@ -255,7 +255,6 @@ class EbicsUserID(models.Model):
Create new keys and certificates for this user Create new keys and certificates for this user
""" """
self.ensure_one() self.ensure_one()
self.ebics_config_id._check_ebics_files()
if self.state != "draft": if self.state != "draft":
raise UserError( raise UserError(
_("Set state to 'draft' before Bank Key (re)initialisation.") _("Set state to 'draft' before Bank Key (re)initialisation.")
@ -442,7 +441,6 @@ class EbicsUserID(models.Model):
must be downloaded and checked for consistency. must be downloaded and checked for consistency.
""" """
self.ensure_one() self.ensure_one()
self.ebics_config_id._check_ebics_files()
if self.state != "get_bank_keys": if self.state != "get_bank_keys":
raise UserError(_("Set state to 'Get Keys from Bank'.")) raise UserError(_("Set state to 'Get Keys from Bank'."))
try: try:

View File

@ -429,8 +429,17 @@ which allows to see all statements downloaded via the ir.cron automated EBICS do
</div> </div>
<ul> <ul>
<li><p class="first">account_ebics_payment_order</p> <li><p class="first">account_ebics_payment_order</p>
<p>Recommended if you are using the OCA account_payment_order module.</p> <p>Required if you are using the OCA account_payment_order module.</p>
<p>Cf. <a class="reference external" href="https://github.com/Noviat/account_ebics">https://github.com/Noviat/account_ebics</a> and <a class="reference external" href="https://github.com/OCA/bank-payment">https://github.com/OCA/bank-payment</a></p> <p>Cf. <a class="reference external" href="https://github.com/OCA/bank-payment">https://github.com/OCA/bank-payment</a></p>
</li>
</ul>
<div class="line-block">
<div class="line"><br /></div>
</div>
<ul>
<li><p class="first">account_ebics_oca_statement_import</p>
<p>Required if you are using the OCA Bank Statement import modules.</p>
<p><a class="reference external" href="https://github.com/OCA/bank-statement-import">https://github.com/OCA/bank-statement-import</a></p>
</li> </li>
</ul> </ul>
<div class="line-block"> <div class="line-block">
@ -454,28 +463,6 @@ which allows to see all statements downloaded via the ir.cron automated EBICS do
<div class="line-block"> <div class="line-block">
<div class="line"><br /></div> <div class="line"><br /></div>
</div> </div>
<ul>
<li><p class="first">account_statement_import_helper</p>
<p>Required if you are processing bank statements with local bank account numbers (e.g. french CFONB files)
and using import parsers based upon the OCA account_statement_import module.</p>
<p>The import helper will match the local bank account number with the IBAN number specified on the Odoo Financial journal.</p>
<p>Cf. <a class="reference external" href="https://github.com/Noviat/noviat-apps">https://github.com/Noviat/noviat-apps</a></p>
</li>
</ul>
<div class="line-block">
<div class="line"><br /></div>
</div>
<ul>
<li><p class="first">account_bank_statement_import_helper</p>
<p>Required if you are processing bank statements with local bank account numbers
and using import parsers based upon the Odoo Enterprise account_bank_statement_import module.</p>
<p>The import helper will match the local bank account number with the IBAN number specified on the Odoo Financial journal.</p>
<p>Cf. <a class="reference external" href="https://github.com/Noviat/noviat-apps">https://github.com/Noviat/noviat-apps</a></p>
</li>
</ul>
<div class="line-block">
<div class="line"><br /></div>
</div>
<div class="section" id="fintech-license"> <div class="section" id="fintech-license">
<h3>Fintech license</h3> <h3>Fintech license</h3>
<p>If you have a valid Fintech.ebics license, you should add the following <p>If you have a valid Fintech.ebics license, you should add the following
@ -488,12 +475,6 @@ licensing parameters to the odoo server configuration file:</p>
<li>fintech_register_keycode</li> <li>fintech_register_keycode</li>
</ul> </ul>
<p>The keycode of the licensed version.</p> <p>The keycode of the licensed version.</p>
<ul class="simple">
<li>fintech_register_users</li>
</ul>
<p>The licensed EBICS user ids. It must be a string or a list of user ids.</p>
<p>You should NOT specify this parameter if your license is subsciption
based (with monthly recurring billing).</p>
<div class="line-block"> <div class="line-block">
<div class="line"><br /></div> <div class="line"><br /></div>
<div class="line">Example:</div> <div class="line">Example:</div>
@ -503,7 +484,6 @@ based (with monthly recurring billing).</p>
; fintech ; fintech
fintech_register_name = MyCompany fintech_register_name = MyCompany
fintech_register_keycode = AB1CD-E2FG-3H-IJ4K-5L fintech_register_keycode = AB1CD-E2FG-3H-IJ4K-5L
fintech_register_users = USER1, USER2
</pre> </pre>
<div class="line-block"> <div class="line-block">
<div class="line"><br /></div> <div class="line"><br /></div>

View File

@ -52,7 +52,6 @@
<field name="ebics_host" /> <field name="ebics_host" />
<field name="ebics_url" /> <field name="ebics_url" />
<field name="ebics_partner" /> <field name="ebics_partner" />
<field name="ebics_files" />
<field name="ebics_keys" /> <field name="ebics_keys" />
</group> </group>
<group name="main-right"> <group name="main-right">

View File

@ -27,7 +27,6 @@ class EbicsAdminOrder(models.TransientModel):
def ebics_admin_order(self): def ebics_admin_order(self):
self.ensure_one() self.ensure_one()
self.ebics_config_id._check_ebics_files()
client = self._setup_client() client = self._setup_client()
if not client: if not client:
self.note += ( self.note += (

View File

@ -10,7 +10,6 @@ logging.basicConfig(
import base64 import base64
import logging import logging
import os
from sys import exc_info from sys import exc_info
from traceback import format_exception from traceback import format_exception
@ -71,9 +70,9 @@ class EbicsXfer(models.TransientModel):
date_from = fields.Date() date_from = fields.Date()
date_to = fields.Date() date_to = fields.Date()
upload_data = fields.Binary(string="File to Upload") upload_data = fields.Binary(string="File to Upload")
upload_fname = fields.Char(default="") upload_fname = fields.Char(string="Upload Filename", default="")
upload_fname_dummy = fields.Char( upload_fname_dummy = fields.Char(
related="upload_fname", string="Upload Filename", readonly=True related="upload_fname", string="Dummy Upload Filename", readonly=True
) )
format_id = fields.Many2one( format_id = fields.Many2one(
comodel_name="ebics.file.format", comodel_name="ebics.file.format",
@ -193,7 +192,6 @@ class EbicsXfer(models.TransientModel):
def ebics_download(self): def ebics_download(self):
self.ensure_one() self.ensure_one()
self.ebics_config_id._check_ebics_files()
ctx = self.env.context.copy() ctx = self.env.context.copy()
self.note = "" self.note = ""
err_cnt = 0 err_cnt = 0
@ -532,43 +530,17 @@ class EbicsXfer(models.TransientModel):
return ebics_files return ebics_files
def _create_ebics_file(self, data, file_format, docname=None): def _create_ebics_file(self, data, file_format, docname=None):
"""
Write the data as received over the EBICS connection
to a temporary file so that is is available for
analysis (e.g. in case formats are received that cannot
be handled in the current version of this module).
TODO: add code to clean-up /tmp on a regular basis.
After saving the data received we call the method to perform
file format specific processing.
"""
ebics_files_root = self.ebics_config_id.ebics_files
tmp_dir = os.path.normpath(ebics_files_root + "/tmp")
if not os.path.isdir(tmp_dir):
os.makedirs(tmp_dir, mode=0o700)
fn_parts = [self.ebics_config_id.ebics_host, self.ebics_config_id.ebics_partner] fn_parts = [self.ebics_config_id.ebics_host, self.ebics_config_id.ebics_partner]
if docname: if docname:
fn_parts.append(docname) fn_parts.append(docname)
else: else:
fn_date = self.date_to or fields.Date.today() fn_date = self.date_to or fields.Date.today()
fn_parts.append(fn_date.isoformat()) fn_parts.append(fn_date.isoformat())
base_fn = "_".join(fn_parts) fn = "_".join(fn_parts)
n = 1
full_tmp_fn = os.path.normpath(tmp_dir + "/" + base_fn)
while os.path.exists(full_tmp_fn):
n += 1
tmp_fn = base_fn + "_" + str(n).rjust(3, "0")
full_tmp_fn = os.path.normpath(tmp_dir + "/" + tmp_fn)
with open(full_tmp_fn, "wb") as f:
f.write(data)
ff_methods = self._file_format_methods() ff_methods = self._file_format_methods()
if file_format.name in ff_methods: if file_format.name in ff_methods:
data = ff_methods[file_format.name](data) data = ff_methods[file_format.name](data)
fn = base_fn
suffix = file_format.suffix suffix = file_format.suffix
if suffix and not fn.endswith(suffix): if suffix and not fn.endswith(suffix):
fn = ".".join([fn, suffix]) fn = ".".join([fn, suffix])

View File

@ -82,7 +82,7 @@
<separator string="Select your file :" colspan="2" /> <separator string="Select your file :" colspan="2" />
<field name="upload_data" filename="upload_fname" required="1" /> <field name="upload_data" filename="upload_fname" required="1" />
<field name="upload_fname" invisible="1" /> <field name="upload_fname" invisible="1" />
<field name="upload_fname_dummy" /> <field name="upload_fname_dummy" string="Upload Filename" />
<field <field
name="format_id" name="format_id"
required="1" required="1"

View File

@ -46,15 +46,35 @@ class AccountStatementImport(models.TransientModel):
show days without transactions via the bank statement list view. show days without transactions via the bank statement list view.
""" """
if self.env.context.get("active_model") == "ebics.file": if self.env.context.get("active_model") == "ebics.file":
messages = []
transactions = False transactions = False
for st_vals in stmts_vals: for st_vals in stmts_vals:
statement_ids = result["statement_ids"][:]
self._set_statement_name(st_vals)
if st_vals.get("transactions"): if st_vals.get("transactions"):
transactions = True transactions = True
break super()._create_bank_statements([st_vals], result)
if not transactions: if result["statement_ids"] == statement_ids:
message = _("This file doesn't contain any transaction.") # no statement has been created, this is the case
st_line_ids = [] # when all transactions have been imported already
notifications = {"type": "warning", "message": message, "details": ""} messages.append(
return st_line_ids, [notifications] _(
"Statement %(st_name)s dated %(date)s "
"has already been imported.",
st_name=st_vals["name"],
date=st_vals["date"].strftime("%Y-%m-%d"),
)
)
return super()._create_bank_statements(stmts_vals, result) if not transactions:
messages.append(_("This file doesn't contain any transaction."))
if messages:
result["notifications"].append(
{"type": "warning", "message": "\n".join(messages)}
)
return
def _set_statement_name(self, st_vals):
"""
Inherit this method to set your own statement naming policy.
"""

View File

@ -22,7 +22,3 @@ Usage
Create your Payment Order and generate the bank file. Create your Payment Order and generate the bank file.
Upload the generated file via the 'EBICS Upload' button on the payment order. Upload the generated file via the 'EBICS Upload' button on the payment order.
Known issues / Roadmap
======================
* Add support for multiple EBICS connections.

View File

@ -12,8 +12,6 @@
"data": [ "data": [
"views/account_payment_order_views.xml", "views/account_payment_order_views.xml",
], ],
# installable False unit OCA payment order becomes
# available for 16.0
"images": ["static/description/cover.png"], "images": ["static/description/cover.png"],
"installable": False, "installable": True,
} }

View File

@ -0,0 +1 @@
../../../../account_ebics_payment_order

View File

@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)