diff --git a/l10n_ch_bank_statement_import_camt/README.rst b/l10n_ch_bank_statement_import_camt/README.rst new file mode 100644 index 000000000..8eb8078e3 --- /dev/null +++ b/l10n_ch_bank_statement_import_camt/README.rst @@ -0,0 +1,97 @@ +================================== +l10n_ch_bank_statement_import_camt +================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:9b0edf16915f6c32eb8a8559d23ee530ed493c2f02dff4e954fef5b166bb137b + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fl10n--switzerland-lightgray.png?logo=github + :target: https://github.com/OCA/l10n-switzerland/tree/14.0/l10n_ch_bank_statement_import_camt + :alt: OCA/l10n-switzerland +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/l10n-switzerland-14-0/l10n-switzerland-14-0-l10n_ch_bank_statement_import_camt + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/l10n-switzerland&target_branch=14.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This addon extend the features of the camt parser in bank-statement-import from OCA by responding to specific need for switzerland (like QRR account). + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Journals can be defined as "QR IBAN for import" which modify the way the import will behave. +If this is marked it will search for a bank account with a l10n_ch_qr_iban corresponding to the second IBAN available in the camt (Ntry/NtryRef xml tag). +This allow to have a bank account set on two different journals and use the QRR swiss functionality to import on his own specific journal the referenced transactions. + +Known issues / Roadmap +====================== + +Known issues: + +* Currently this module has a problem with the manual import (Specific import button on each journal) when two journals are set with the same bank account. This is because the match journal check that the context journal is equal to the one matched, it matches the first journal with the IBAN found in the file. The general import button does work. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Compassion CH + +Contributors +~~~~~~~~~~~~ + +* Simon Gonzalez (Compassion CH) + +Other credits +~~~~~~~~~~~~~ + +The development of this module has been financially supported by: + +* Compassion CH + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/l10n-switzerland `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/l10n_ch_bank_statement_import_camt/__init__.py b/l10n_ch_bank_statement_import_camt/__init__.py new file mode 100644 index 000000000..9b4296142 --- /dev/null +++ b/l10n_ch_bank_statement_import_camt/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/l10n_ch_bank_statement_import_camt/__manifest__.py b/l10n_ch_bank_statement_import_camt/__manifest__.py new file mode 100644 index 000000000..f5d8799f1 --- /dev/null +++ b/l10n_ch_bank_statement_import_camt/__manifest__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Compassion CH +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "l10n_ch_bank_statement_import_camt", + "version": "14.0.1.0.0", + "category": "Account", + "website": "https://github.com/OCA/l10n-switzerland", + "author": "Compassion CH, " "Odoo Community Association (OCA)", + "license": "AGPL-3", + "installable": True, + "data": ["views/account_journal_view.xml"], + "depends": ["account_statement_import_camt54", "l10n_ch_qriban"], +} diff --git a/l10n_ch_bank_statement_import_camt/models/__init__.py b/l10n_ch_bank_statement_import_camt/models/__init__.py new file mode 100644 index 000000000..265bdf7c7 --- /dev/null +++ b/l10n_ch_bank_statement_import_camt/models/__init__.py @@ -0,0 +1,2 @@ +from . import account_journal +from . import custom_parser diff --git a/l10n_ch_bank_statement_import_camt/models/account_journal.py b/l10n_ch_bank_statement_import_camt/models/account_journal.py new file mode 100644 index 000000000..af621b01a --- /dev/null +++ b/l10n_ch_bank_statement_import_camt/models/account_journal.py @@ -0,0 +1,27 @@ +# Copyright 2023 Compassion CH +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class AccountJournal(models.Model): + _inherit = "account.journal" + + should_qr_parsing = fields.Boolean( + string="QR IBAN for import", + help="Parse the account QR iban field for CAMT54\n" + "This field can't accept three journals with the same account number.", + ) + + @api.constrains("should_qr_parsing") + def _qr_iban_defined_qr_parsing(self): + for journal in self: + if ( + journal.should_qr_parsing + and not journal.bank_account_id.l10n_ch_qr_iban + ): + raise ValueError( + "The QR IBAN should be defined on the bank account " + "if you use QR IBAN for import on the journal" + ) + return True diff --git a/l10n_ch_bank_statement_import_camt/models/custom_parser.py b/l10n_ch_bank_statement_import_camt/models/custom_parser.py new file mode 100644 index 000000000..22894a652 --- /dev/null +++ b/l10n_ch_bank_statement_import_camt/models/custom_parser.py @@ -0,0 +1,215 @@ +import re +import string + +from odoo import _, models + + +class CustomParser(models.AbstractModel): + _inherit = "account.statement.import.camt.parser" + + def parse_entry(self, ns, node): + """Parse an Ntry node and yield transactions""" + + # Get some basic infos about the transaction in the XML. + transaction = { + "payment_ref": "/", + "amount": 0, + "narration": {}, + "transaction_type": {}, + } + + self.add_value_from_node( + ns, node, "./ns:BkTxCd/ns:Prtry/ns:Cd", transaction, "transaction_type" + ) + self.add_value_from_node(ns, node, "./ns:BookgDt/ns:Dt", transaction, "date") + amount = self.parse_amount(ns, node) + if amount != 0.0: + transaction["amount"] = amount + self.add_value_from_node( + ns, node, "./ns:AddtlNtryInf", transaction, "payment_ref" + ) + self.add_value_from_node( + ns, + node, + [ + "./ns:NtryDtls/ns:RmtInf/ns:Strd/ns:CdtrRefInf/ns:Ref", + "./ns:NtryDtls/ns:Btch/ns:PmtInfId", + "./ns:NtryDtls/ns:TxDtls/ns:Refs/ns:AcctSvcrRef", + ], + transaction["narration"], + "%s (NtryDtls/Btch/PmtInfId)" % _("Account Servicer Reference"), + ) + + # save AddtlNtryInf for after to add it to name of transaction + addtl_ntry_inf = node.xpath("./ns:AddtlNtryInf", namespaces={"ns": ns}) + + node_charge_amount = node.xpath( + "./ns:Chrgs/ns:Rcrd/ns:Amt", namespaces={"ns": ns} + ) + node_charge_included = node.xpath( + "./ns:Chrgs/ns:Rcrd/ns:ChrgInclInd", namespaces={"ns": ns} + ) + + # has a charge and is not included + if len(node_charge_included) > 0 and node_charge_included[0].text == "true": + if len(node_charge_amount) > 0: + charge_amount = -float(node_charge_amount[0].text) + tr = transaction.copy() + tr["amount"] = charge_amount + tr["payment_ref"] += " (bank charge)" + yield tr + + # If there is a 'TxDtls' node in the XML we get the value of + # 'AcctSvcrRef' in it. + details_nodes = node.xpath("./ns:NtryDtls/ns:TxDtls", namespaces={"ns": ns}) + if len(details_nodes) == 0: + yield transaction + self.add_value_from_node( + ns, + node, + "./ns:AcctSvcrRef", + transaction["narration"], + "%s (AcctSvcrRef)" % _("Account Servicer Reference"), + ) + return + + self.add_value_from_node( + ns, + node, + "./ns:BkTxCd/ns:Domn/ns:Fmly/ns:SubFmlyCd", + transaction["narration"], + "%s (BkTxCd/Domn/Fmly/SubFmlyCd)" % _("Sub Family Code"), + ) + + self.add_value_from_node( + ns, + node, + "./ns:NtryDtls/ns:TxDtls/ns:Refs/ns:EndToEndId", + transaction["narration"], + "%s (NtryDtls/TxDtls/Refs/EndToEndId)" % _("End To End Identification"), + ) + + transaction_base = transaction + for node in details_nodes: + transaction = transaction_base.copy() + self.parse_transaction_details(ns, node, transaction) + + # If there is a AddtlNtryInf, then we do the concatenate + if addtl_ntry_inf: + transaction["payment_ref"] += f" - [{addtl_ntry_inf[0].text}]" + yield transaction + self.generate_narration(transaction) + + def parse_transaction_details(self, ns, node, transaction): + super().parse_transaction_details(ns, node, transaction) + # Check if a global AcctSvcrRef exist + found_node = node.xpath("../../ns:AcctSvcrRef", namespaces={"ns": ns}) + if len(found_node) != 0: + self.add_value_from_node( + ns, + node, + "../../ns:AcctSvcrRef", + transaction["narration"], + "%s (../../AcctSvcrRef)" % _("Account Servicer Reference"), + ) + else: + self.add_value_from_node( + ns, + node, + "./ns:Refs/ns:AcctSvcrRef", + transaction["narration"], + "%s (./Refs/AcctSvcrRef)" % _("Account Servicer Reference"), + ) + # Add transaction note for QR statements + self.add_value_from_node( + ns, + node, + [ + "./ns:RmtInf/ns:Strd/ns:AddtlRmtInf", + ], + transaction, + "note", + join_str="\n", + ) + + def parse_statement(self, ns, node): + result = {} + + iban = node.xpath("./ns:Acct/ns:Id/ns:IBAN", namespaces={"ns": ns}) + if len(iban) > 0: + # find the journal related to this account + journals = self.env["account.journal"].search([]) + trans_table = str.maketrans("", "", string.whitespace) + account_number_ = iban[0].text.translate(trans_table) + journal = journals.filtered( + lambda x: x.bank_acc_number + and x.bank_acc_number.translate(trans_table) == account_number_ + and not x.should_qr_parsing + ) + if journal: + self = self.with_context(journal_id=journal.id) + + entry_nodes = node.xpath("./ns:Ntry", namespaces={"ns": ns}) + if len(entry_nodes) > 0: + result = super().parse_statement(ns, node) + + entry_ref = node.xpath("./ns:Ntry/ns:NtryRef", namespaces={"ns": ns}) + if len(entry_ref) > 1 and "054" in ns: + first_entry = entry_ref[0].text + # Parse all entry ref node to check if they're all the same. + for entry in entry_ref: + if first_entry != entry.text: + raise ValueError( + "Different entry ref in same file " "not supported !" + ) + self.add_value_from_node( + ns, node, "./ns:Ntry/ns:NtryRef", result, "ntryRef" + ) + result["camt_headers"] = ns + # In case of an empty camt file + else: + result["transactions"] = "" + result["is_empty"] = True + return result + + def parse(self, data): + result = super().parse(data) + currency = result[0] + account_number = result[1] + statements = result[2] + if len(statements) > 0: + if "camt_headers" in statements[0]: + if "camt.054" not in statements[0]["camt_headers"]: + statements[0].pop("camt_headers") + if "ntryRef" in statements[0]: + statements[0].pop("ntryRef") + return currency, account_number, statements + + def get_balance_amounts(self, ns, node): + result = super().get_balance_amounts(ns, node) + start_balance_node = result[0] + end_balance_node = result[0] + + details_nodes = node.xpath("./ns:Bal/ns:Amt", namespaces={"ns": ns}) + + if start_balance_node == 0.0 and not len(details_nodes): + start_balance_node = node.xpath("./ns:Ntry", namespaces={"ns": ns}) + amount_tot = 0 + for node in start_balance_node: + amount_tot -= self.parse_amount(ns, node) + return (amount_tot, end_balance_node) + return result + + def check_version(self, ns, root): + try: + super().check_version(ns, root) + except ValueError: + re_camt_version = re.compile( + r"(^urn:iso:std:iso:20022:tech:xsd:camt.054." r"|^ISO:camt.054.)" + ) + if not re_camt_version.search(ns): + raise ValueError("no camt 052 or 053 or 054: " + ns) + # Check GrpHdr element: + root_0_0 = root[0][0].tag[len(ns) + 2 :] # strip namespace + if root_0_0 != "GrpHdr": + raise ValueError("expected GrpHdr, got: " + root_0_0) diff --git a/l10n_ch_bank_statement_import_camt/readme/CONTRIBUTORS.rst b/l10n_ch_bank_statement_import_camt/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..b347d52e4 --- /dev/null +++ b/l10n_ch_bank_statement_import_camt/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Simon Gonzalez (Compassion CH) diff --git a/l10n_ch_bank_statement_import_camt/readme/CREDITS.rst b/l10n_ch_bank_statement_import_camt/readme/CREDITS.rst new file mode 100644 index 000000000..6b243c31e --- /dev/null +++ b/l10n_ch_bank_statement_import_camt/readme/CREDITS.rst @@ -0,0 +1,3 @@ +The development of this module has been financially supported by: + +* Compassion CH diff --git a/l10n_ch_bank_statement_import_camt/readme/DESCRIPTION.rst b/l10n_ch_bank_statement_import_camt/readme/DESCRIPTION.rst new file mode 100644 index 000000000..a67e6bcc1 --- /dev/null +++ b/l10n_ch_bank_statement_import_camt/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This addon extend the features of the camt parser in bank-statement-import from OCA by responding to specific need for switzerland (like QRR account). diff --git a/l10n_ch_bank_statement_import_camt/readme/ROADMAP.rst b/l10n_ch_bank_statement_import_camt/readme/ROADMAP.rst new file mode 100644 index 000000000..a3f698c64 --- /dev/null +++ b/l10n_ch_bank_statement_import_camt/readme/ROADMAP.rst @@ -0,0 +1,3 @@ +Known issues: + +* Currently this module has a problem with the manual import (Specific import button on each journal) when two journals are set with the same bank account. This is because the match journal check that the context journal is equal to the one matched, it matches the first journal with the IBAN found in the file. The general import button does work. diff --git a/l10n_ch_bank_statement_import_camt/readme/USAGE.rst b/l10n_ch_bank_statement_import_camt/readme/USAGE.rst new file mode 100644 index 000000000..2e03c2428 --- /dev/null +++ b/l10n_ch_bank_statement_import_camt/readme/USAGE.rst @@ -0,0 +1,3 @@ +Journals can be defined as "QR IBAN for import" which modify the way the import will behave. +If this is marked it will search for a bank account with a l10n_ch_qr_iban corresponding to the second IBAN available in the camt (Ntry/NtryRef xml tag). +This allow to have a bank account set on two different journals and use the QRR swiss functionality to import on his own specific journal the referenced transactions. diff --git a/l10n_ch_bank_statement_import_camt/static/description/index.html b/l10n_ch_bank_statement_import_camt/static/description/index.html new file mode 100644 index 000000000..d4c2007c4 --- /dev/null +++ b/l10n_ch_bank_statement_import_camt/static/description/index.html @@ -0,0 +1,444 @@ + + + + + + +l10n_ch_bank_statement_import_camt + + + +
+

l10n_ch_bank_statement_import_camt

+ + +

Beta License: AGPL-3 OCA/l10n-switzerland Translate me on Weblate Try me on Runboat

+

This addon extend the features of the camt parser in bank-statement-import from OCA by responding to specific need for switzerland (like QRR account).

+

Table of contents

+ +
+

Usage

+

Journals can be defined as “QR IBAN for import” which modify the way the import will behave. +If this is marked it will search for a bank account with a l10n_ch_qr_iban corresponding to the second IBAN available in the camt (Ntry/NtryRef xml tag). +This allow to have a bank account set on two different journals and use the QRR swiss functionality to import on his own specific journal the referenced transactions.

+
+
+

Known issues / Roadmap

+

Known issues:

+
    +
  • Currently this module has a problem with the manual import (Specific import button on each journal) when two journals are set with the same bank account. This is because the match journal check that the context journal is equal to the one matched, it matches the first journal with the IBAN found in the file. The general import button does work.
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Compassion CH
  • +
+
+
+

Contributors

+
    +
  • Simon Gonzalez (Compassion CH)
  • +
+
+
+

Other credits

+

The development of this module has been financially supported by:

+
    +
  • Compassion CH
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/l10n-switzerland project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/l10n_ch_bank_statement_import_camt/views/account_journal_view.xml b/l10n_ch_bank_statement_import_camt/views/account_journal_view.xml new file mode 100644 index 000000000..50cff5bef --- /dev/null +++ b/l10n_ch_bank_statement_import_camt/views/account_journal_view.xml @@ -0,0 +1,15 @@ + + + + account.journal + + + + + + + + diff --git a/l10n_ch_bank_statement_import_camt/wizard/__init__.py b/l10n_ch_bank_statement_import_camt/wizard/__init__.py new file mode 100644 index 000000000..ae69bca27 --- /dev/null +++ b/l10n_ch_bank_statement_import_camt/wizard/__init__.py @@ -0,0 +1 @@ +from . import account_statement_import diff --git a/l10n_ch_bank_statement_import_camt/wizard/account_statement_import.py b/l10n_ch_bank_statement_import_camt/wizard/account_statement_import.py new file mode 100644 index 000000000..e716ca4f6 --- /dev/null +++ b/l10n_ch_bank_statement_import_camt/wizard/account_statement_import.py @@ -0,0 +1,79 @@ +# Copyright 2023 Compassion CH +# Licence LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0). + +import logging + +from odoo import api, fields, models + +from odoo.addons.base.models.res_bank import sanitize_account_number + +logger = logging.getLogger(__name__) + + +class ResPartnerBank(models.Model): + _inherit = "res.partner.bank" + + sanitized_qr_iban = fields.Char(compute="_compute_sanitized_acc_number", store=True) + + @api.depends("l10n_ch_qr_iban") + def _compute_sanitized_acc_number(self): + for bank in self: + bank.sanitized_qr_iban = ( + sanitize_account_number(bank.l10n_ch_qr_iban) + if bank.l10n_ch_qr_iban + else False + ) + + +class AccountStatementImport(models.TransientModel): + _inherit = "account.statement.import" + _description = "Import Bank Statement Files" + + qriban_no = fields.Char(readonly=True, invisible=True) + + def _check_parsed_data(self, stmts_vals): + """Retrieve the information from the statement vals and take it out of the datas""" + if stmts_vals: + if stmts_vals[0].get("camt_headers"): + stmts_vals[0].pop("camt_headers") + ntry_ref = stmts_vals[0].pop("ntryRef") + if ntry_ref: + self.write({"qriban_no": ntry_ref}) + return super(AccountStatementImport, self)._check_parsed_data(stmts_vals) + + @api.model + def _match_journal(self, account_number, currency): + """ + We want to match the journal that are with qr iban in some cases + (parsing camt54) + """ + journal = super()._match_journal(account_number, currency) + journal_obj = self.env["account.journal"] + journal_qriban = self.qriban_no + if journal_qriban: + company = self.env.company + bank_account = self.env["res.partner.bank"].search( + [ + ("partner_id", "=", company.partner_id.id), + ("sanitized_qr_iban", "=", sanitize_account_number(journal_qriban)), + ], + limit=1, + ) + journal = journal_obj.search( + [ + ("type", "=", "bank"), + ("bank_account_id", "=", bank_account.id), + ("should_qr_parsing", "=", True), + ], + limit=1, + ) + elif journal and journal.should_qr_parsing: + journal = journal_obj.search( + [ + ("type", "=", "bank"), + ("bank_account_id", "=", journal.bank_account_id.id), + ("should_qr_parsing", "=", False), + ], + limit=1, + ) + return journal diff --git a/setup/l10n_ch_bank_statement_import_camt/odoo/addons/l10n_ch_bank_statement_import_camt b/setup/l10n_ch_bank_statement_import_camt/odoo/addons/l10n_ch_bank_statement_import_camt new file mode 120000 index 000000000..51ae8f046 --- /dev/null +++ b/setup/l10n_ch_bank_statement_import_camt/odoo/addons/l10n_ch_bank_statement_import_camt @@ -0,0 +1 @@ +../../../../l10n_ch_bank_statement_import_camt \ No newline at end of file diff --git a/setup/l10n_ch_bank_statement_import_camt/setup.py b/setup/l10n_ch_bank_statement_import_camt/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/l10n_ch_bank_statement_import_camt/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)