From 20ed7338ca536b39dfea359323e52908a275f1a9 Mon Sep 17 00:00:00 2001 From: Simon Gonzalez <45272875+simgonzalez@users.noreply.github.com> Date: Thu, 29 Jun 2023 12:57:48 +0200 Subject: [PATCH] 14.0 l10n ch import payment return (#66) * [ADD] l10n_ch_import_payment_return module to handle return from payment orders Add functionalities to handle the payment return from the switzerland pain generated from payment orders --- l10n_ch_import_payment_return/README.rst | 86 ++++++++++++++++ l10n_ch_import_payment_return/__init__.py | 2 + l10n_ch_import_payment_return/__manifest__.py | 16 +++ .../components/__init__.py | 1 + .../components/edi_import_payment_return.py | 31 ++++++ .../data/edi_data.xml | 7 ++ .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 1 + .../readme/HISTORY.rst | 4 + .../readme/ROADMAP.rst | 1 + .../readme/USAGE.rst | 9 ++ .../wizard/__init__.py | 1 + .../wizard/pain_parser_ch.py | 97 +++++++++++++++++++ .../wizard/payment_return_import.py | 33 +++++++ 14 files changed, 290 insertions(+) create mode 100644 l10n_ch_import_payment_return/README.rst create mode 100644 l10n_ch_import_payment_return/__init__.py create mode 100644 l10n_ch_import_payment_return/__manifest__.py create mode 100644 l10n_ch_import_payment_return/components/__init__.py create mode 100644 l10n_ch_import_payment_return/components/edi_import_payment_return.py create mode 100644 l10n_ch_import_payment_return/data/edi_data.xml create mode 100644 l10n_ch_import_payment_return/readme/CONTRIBUTORS.rst create mode 100644 l10n_ch_import_payment_return/readme/DESCRIPTION.rst create mode 100644 l10n_ch_import_payment_return/readme/HISTORY.rst create mode 100644 l10n_ch_import_payment_return/readme/ROADMAP.rst create mode 100644 l10n_ch_import_payment_return/readme/USAGE.rst create mode 100644 l10n_ch_import_payment_return/wizard/__init__.py create mode 100644 l10n_ch_import_payment_return/wizard/pain_parser_ch.py create mode 100644 l10n_ch_import_payment_return/wizard/payment_return_import.py diff --git a/l10n_ch_import_payment_return/README.rst b/l10n_ch_import_payment_return/README.rst new file mode 100644 index 000000000..df20efb10 --- /dev/null +++ b/l10n_ch_import_payment_return/README.rst @@ -0,0 +1,86 @@ +============================= +l10n_ch_import_payment_return +============================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:74050c117e767ff97c56412206efd3981573666cdf9205e8b4c1b71ae7b09a8f + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-CompassionCH%2Fl10n_switzerland-lightgray.png?logo=github + :target: https://github.com/CompassionCH/l10n_switzerland/tree/14.0/l10n_ch_import_payment_return + :alt: CompassionCH/l10n_switzerland + +|badge1| |badge2| |badge3| + +This module add the functionality of processing the return of the payment orders + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To use this module, you need to: + +#. Go to EDI and configure a backend and an exchange type + +#. Create a payment order and choose an EDI exchange type + +#. When generating the file an edi exchange should be created + +#. When a return come back it should be processed by the parser + +Known issues / Roadmap +====================== + +* Automate a generic treatment process of the files + +Changelog +========= + +14.0.1.0.0 (2023-06-23) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [ADD] l10n_ch_import_payment_return + +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 (https://compassion.ch) + +Maintainers +~~~~~~~~~~~ + +This module is part of the `CompassionCH/l10n_switzerland `_ project on GitHub. + +You are welcome to contribute. diff --git a/l10n_ch_import_payment_return/__init__.py b/l10n_ch_import_payment_return/__init__.py new file mode 100644 index 000000000..d3c1316ee --- /dev/null +++ b/l10n_ch_import_payment_return/__init__.py @@ -0,0 +1,2 @@ +from . import wizard +from . import components diff --git a/l10n_ch_import_payment_return/__manifest__.py b/l10n_ch_import_payment_return/__manifest__.py new file mode 100644 index 000000000..5b84337b9 --- /dev/null +++ b/l10n_ch_import_payment_return/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2023 Compassion CH () +# @author: Simon Gonzalez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + "name": "l10n_ch_import_payment_return", + "version": "14.0.1.0.0", + "development_status": "Beta", + "license": "AGPL-3", + "author": "Compassion CH,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/l10n-switzerland", + "category": "Banking addons", + "depends": ["account_payment_export_sftp"], # OCA/bank-payment + "data": ["data/edi_data.xml"], + "installable": True, +} diff --git a/l10n_ch_import_payment_return/components/__init__.py b/l10n_ch_import_payment_return/components/__init__.py new file mode 100644 index 000000000..52083cc35 --- /dev/null +++ b/l10n_ch_import_payment_return/components/__init__.py @@ -0,0 +1 @@ +from . import edi_import_payment_return diff --git a/l10n_ch_import_payment_return/components/edi_import_payment_return.py b/l10n_ch_import_payment_return/components/edi_import_payment_return.py new file mode 100644 index 000000000..5d3c4b4b5 --- /dev/null +++ b/l10n_ch_import_payment_return/components/edi_import_payment_return.py @@ -0,0 +1,31 @@ +# Copyright 2023 Compassion CH (https://www.compassion.ch) +# @author: Simon Gonzalez +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import base64 + +from odoo.addons.component.core import Component + +from ..wizard.pain_parser_ch import PainParserCH + + +class EDIExchangeProcessPaymentReturn(Component): + """ACK for payment order pain002.01.03.CH""" + + _name = "edi.input.payment.ch.process" + _inherit = "edi.component.input.mixin" + _backend_type = "sftp_pay_ord_ack" + _usage = "input.process" + + def process(self): + origin_output = self.exchange_record.parent_id + payment_order = self.env[origin_output.model].browse( + self.exchange_record.parent_id.res_id + ) + try: + PainParserCH().validate_initial_return( + base64.b64decode(self.exchange_record.exchange_file) + ) + except Exception as e: + payment_order.action_cancel() + raise e + payment_order.generated2uploaded() diff --git a/l10n_ch_import_payment_return/data/edi_data.xml b/l10n_ch_import_payment_return/data/edi_data.xml new file mode 100644 index 000000000..b1330b5ca --- /dev/null +++ b/l10n_ch_import_payment_return/data/edi_data.xml @@ -0,0 +1,7 @@ + + + + SFTP for payment orders with ack + sftp_pay_ord_ack + + diff --git a/l10n_ch_import_payment_return/readme/CONTRIBUTORS.rst b/l10n_ch_import_payment_return/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..afb8930b2 --- /dev/null +++ b/l10n_ch_import_payment_return/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Simon Gonzalez (https://compassion.ch) diff --git a/l10n_ch_import_payment_return/readme/DESCRIPTION.rst b/l10n_ch_import_payment_return/readme/DESCRIPTION.rst new file mode 100644 index 000000000..d6dd602a9 --- /dev/null +++ b/l10n_ch_import_payment_return/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module add the functionality of processing the return of the payment orders diff --git a/l10n_ch_import_payment_return/readme/HISTORY.rst b/l10n_ch_import_payment_return/readme/HISTORY.rst new file mode 100644 index 000000000..94b750596 --- /dev/null +++ b/l10n_ch_import_payment_return/readme/HISTORY.rst @@ -0,0 +1,4 @@ +14.0.1.0.0 (2023-06-23) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [ADD] l10n_ch_import_payment_return diff --git a/l10n_ch_import_payment_return/readme/ROADMAP.rst b/l10n_ch_import_payment_return/readme/ROADMAP.rst new file mode 100644 index 000000000..1904805be --- /dev/null +++ b/l10n_ch_import_payment_return/readme/ROADMAP.rst @@ -0,0 +1 @@ +* Automate a generic treatment process of the files diff --git a/l10n_ch_import_payment_return/readme/USAGE.rst b/l10n_ch_import_payment_return/readme/USAGE.rst new file mode 100644 index 000000000..030fed03c --- /dev/null +++ b/l10n_ch_import_payment_return/readme/USAGE.rst @@ -0,0 +1,9 @@ +To use this module, you need to: + +#. Go to EDI and configure a backend and an exchange type + +#. Create a payment order and choose an EDI exchange type + +#. When generating the file an edi exchange should be created + +#. When a return come back it should be processed by the parser diff --git a/l10n_ch_import_payment_return/wizard/__init__.py b/l10n_ch_import_payment_return/wizard/__init__.py new file mode 100644 index 000000000..3f4a01875 --- /dev/null +++ b/l10n_ch_import_payment_return/wizard/__init__.py @@ -0,0 +1 @@ +from . import payment_return_import diff --git a/l10n_ch_import_payment_return/wizard/pain_parser_ch.py b/l10n_ch_import_payment_return/wizard/pain_parser_ch.py new file mode 100644 index 000000000..977335576 --- /dev/null +++ b/l10n_ch_import_payment_return/wizard/pain_parser_ch.py @@ -0,0 +1,97 @@ +# Copyright 2023 Compassion CH - Simon Gonzalez +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +import re + +from lxml import etree + +from odoo.addons.account_payment_return_import_iso20022.wizard.pain_parser import ( + PainParser, +) + +_logger = logging.getLogger(__name__) + +ACCEPTANCE_STATUS = ["ACCP", "ACWC", "ACTC"] + + +class AcceptedEmptyByBankException(ValueError): + pass + + +class PainParserCH(PainParser): + """Parser for SEPA Direct Debit Unpaid Report import files.""" + + def _get_root_ns(self, data): + try: + root = etree.fromstring(data, parser=etree.XMLParser(recover=True)) + except etree.XMLSyntaxError: + # ABNAmro is known to mix up encodings + root = etree.fromstring(data.decode("iso-8859-15").encode("utf-8")) + if root is None: + raise ValueError("Not a valid xml file, or not an xml file at all.") + ns = root.tag[1 : root.tag.index("}")] + self.check_version(ns, root) + return root, ns + + def get_origin_msg(self, file): + root, ns = self._get_root_ns(file) + ORIGIN_MSG_XML_NODE = ( + "./ns:CstmrPmtStsRpt/ns:OrgnlGrpInfAndSts/ns:OrgnlMsgId", + ) + found_node = root.xpath(ORIGIN_MSG_XML_NODE, namespaces={"ns": ns}) + return found_node[0].text + + def check_version(self, ns, root): + """Validate validity of SEPA Direct Debit Unpaid Report file.""" + # Check wether it is SEPA Direct Debit Unpaid Report at all: + re_pain = re.compile(r"^http://www.six-interbank-clearing.com/.*/pain") + if not re_pain.search(ns): + raise ValueError("no pain: " + ns) + # Check wether version 002.001.03.ch: + re_pain_version = re.compile(r"|pain.002.001.03.ch.02.xsd") + if not re_pain_version.search(ns): + raise ValueError("no PAIN.002.001.03: " + 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) + + def validate_grp_status(self, ns, root): + """Validate the status of the pain 002""" + STATUS_XML_NODE = "./ns:CstmrPmtStsRpt/ns:OrgnlGrpInfAndSts/ns:GrpSts" + found_node = root.xpath(STATUS_XML_NODE, namespaces={"ns": ns}) + if found_node: + return_status = found_node[0].text + _logger.info(f"File has this status {return_status}") + if return_status in ACCEPTANCE_STATUS: + return True + elif return_status == "PART": + return True + elif return_status == "RJCT": + info_status = root.xpath( + "./ns:CstmrPmtStsRpt/ns:OrgnlGrpInfAndSts/ns:StsRsnInf/ns:AddtlInf", + namespaces={"ns": ns}, + ) + raise ValueError(f"File rejected !\nReason: {info_status[0].text}") + else: + raise ValueError("Status not known by Pain Parser CH") + else: + raise ValueError("Status Node not found") + + def validate_initial_return(self, data): + root, ns = self._get_root_ns(data) + self.validate_grp_status(ns, root) + return root, ns + + def parse(self, data): + """Parse a pain.002.001.03 file.""" + root, ns = self.validate_initial_return(data) + payment_returns = [] + for node in root: + payment_return = super().parse_payment_return(ns, node) + if payment_return["transactions"]: + if not payment_return.get("account_number"): + payment_return["account_number"] = False + payment_returns.append(payment_return) + return payment_returns diff --git a/l10n_ch_import_payment_return/wizard/payment_return_import.py b/l10n_ch_import_payment_return/wizard/payment_return_import.py new file mode 100644 index 000000000..492cba634 --- /dev/null +++ b/l10n_ch_import_payment_return/wizard/payment_return_import.py @@ -0,0 +1,33 @@ +# Copyright 2016 Tecnativa - Carlos Dauden +# Copyright 2019 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo import api, models + +from .pain_parser_ch import PainParserCH + +_logger = logging.getLogger(__name__) + + +class PaymentReturnImport(models.TransientModel): + _inherit = "payment.return.import" + + @api.model + def _parse_single_document(self, data_file): + """ + Try to parse the file as the following format or fall back on next + parser: + - pain.002.001.03.CH + """ + pain_parser = PainParserCH() + try: + _logger.debug("Try parsing as a PAIN Direct Debit Unpaid CH " "Report.") + return pain_parser.parse(data_file) + except ValueError: + _logger.debug( + "Payment return file is not a ISO20022 CH " "supported file.", + exc_info=True, + ) + return super()._parse_single_document(data_file)