From 1529cd47998e98642608654be6aa79f00e40a9be Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Thu, 11 Apr 2024 17:25:58 +0200 Subject: [PATCH 1/2] [ADD] payment_monetico --- payment_monetico/README.rst | 79 ++++ payment_monetico/__init__.py | 2 + payment_monetico/__manifest__.py | 21 + payment_monetico/controllers/__init__.py | 1 + payment_monetico/controllers/main.py | 73 +++ .../data/payment_acquirer_data.xml | 40 ++ payment_monetico/models/__init__.py | 1 + payment_monetico/models/payment.py | 269 +++++++++++ payment_monetico/readme/CONTRIBUTORS.rst | 3 + payment_monetico/readme/DESCRIPTION.rst | 2 + .../static/description/index.html | 423 ++++++++++++++++++ payment_monetico/static/src/img/logo.png | Bin 0 -> 3559 bytes payment_monetico/tests/__init__.py | 1 + payment_monetico/tests/test_monetico.py | 176 ++++++++ .../views/payment_monetico_templates.xml | 34 ++ payment_monetico/views/payment_views.xml | 49 ++ .../odoo/addons/payment_monetico | 1 + setup/payment_monetico/setup.py | 6 + 18 files changed, 1181 insertions(+) create mode 100644 payment_monetico/README.rst create mode 100644 payment_monetico/__init__.py create mode 100644 payment_monetico/__manifest__.py create mode 100644 payment_monetico/controllers/__init__.py create mode 100644 payment_monetico/controllers/main.py create mode 100644 payment_monetico/data/payment_acquirer_data.xml create mode 100644 payment_monetico/models/__init__.py create mode 100644 payment_monetico/models/payment.py create mode 100644 payment_monetico/readme/CONTRIBUTORS.rst create mode 100644 payment_monetico/readme/DESCRIPTION.rst create mode 100644 payment_monetico/static/description/index.html create mode 100644 payment_monetico/static/src/img/logo.png create mode 100644 payment_monetico/tests/__init__.py create mode 100644 payment_monetico/tests/test_monetico.py create mode 100644 payment_monetico/views/payment_monetico_templates.xml create mode 100644 payment_monetico/views/payment_views.xml create mode 120000 setup/payment_monetico/odoo/addons/payment_monetico create mode 100644 setup/payment_monetico/setup.py diff --git a/payment_monetico/README.rst b/payment_monetico/README.rst new file mode 100644 index 000000000..5823033b5 --- /dev/null +++ b/payment_monetico/README.rst @@ -0,0 +1,79 @@ +========================= +Monetico Payment Acquirer +========================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:91e6c262ca7224be33209224b2b47eb7926fdf58181691b6a4615aded5a21674 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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--france-lightgray.png?logo=github + :target: https://github.com/OCA/l10n-france/tree/14.0/payment_monetico + :alt: OCA/l10n-france +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/l10n-france-14-0/l10n-france-14-0-payment_monetico + :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-france&target_branch=14.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Payment Acquirer for `Monetico `_ french payment gateway. + + +**Table of contents** + +.. contents:: + :local: + +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 +~~~~~~~ + +* Akretion + +Contributors +~~~~~~~~~~~~ + +* `Akretion `_: + + * Florian Mounier + +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-france `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/payment_monetico/__init__.py b/payment_monetico/__init__.py new file mode 100644 index 000000000..91c5580fe --- /dev/null +++ b/payment_monetico/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/payment_monetico/__manifest__.py b/payment_monetico/__manifest__.py new file mode 100644 index 000000000..e6dc1f95f --- /dev/null +++ b/payment_monetico/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Monetico Payment Acquirer", + "summary": "Accept payments with Monetico secure payment gateway.", + "version": "14.0.1.0.0", + "license": "AGPL-3", + "category": "Accounting", + "author": "Akretion,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/l10n-france", + "depends": ["payment"], + "data": [ + "views/payment_views.xml", + "views/payment_monetico_templates.xml", + "data/payment_acquirer_data.xml", + ], + "images": ["static/description/icon.png"], + "installable": True, +} diff --git a/payment_monetico/controllers/__init__.py b/payment_monetico/controllers/__init__.py new file mode 100644 index 000000000..12a7e529b --- /dev/null +++ b/payment_monetico/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/payment_monetico/controllers/main.py b/payment_monetico/controllers/main.py new file mode 100644 index 000000000..51698970a --- /dev/null +++ b/payment_monetico/controllers/main.py @@ -0,0 +1,73 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +import pprint + +import werkzeug + +from odoo import http +from odoo.http import request + +_logger = logging.getLogger(__name__) + + +class MoneticoController(http.Controller): + _notify_url = "/payment/monetico/webhook/" + _return_url = "/payment/monetico/return/" + + def monetico_validate_data(self, **post): + monetico = request.env["payment.acquirer"].search( + [("provider", "=", "monetico")], limit=1 + ) + values = dict(post) + shasign = values.pop("MAC", False) + if shasign.upper() != monetico._monetico_generate_shasign(values).upper(): + _logger.debug("Monetico: validated data") + return ( + request.env["payment.transaction"] + .sudo() + .form_feedback(post, "monetico") + ) + _logger.warning("Monetico: data are corrupted") + return False + + @http.route( + "/payment/monetico/webhook/", + type="http", + auth="public", + methods=["POST"], + csrf=False, + ) + def monetico_webhook(self, **post): + """Monetico IPN.""" + _logger.info( + "Beginning Monetico IPN form_feedback with post data %s", + pprint.pformat(post), + ) + if not post: + _logger.warning("Monetico: received empty notification; skip.") + else: + self.monetico_validate_data(**post) + return "" + + @http.route( + "/payment/monetico/return", + type="http", + auth="public", + methods=["POST"], + csrf=False, + save_session=False, + ) + def monetico_return(self, **post): + """Monetico DPN.""" + try: + _logger.info( + "Beginning Monetico DPN form_feedback with post data %s", + pprint.pformat(post), + ) + self.monetico_validate_data(**post) + except Exception: + pass + return werkzeug.utils.redirect("/payment/process") diff --git a/payment_monetico/data/payment_acquirer_data.xml b/payment_monetico/data/payment_acquirer_data.xml new file mode 100644 index 000000000..522af1372 --- /dev/null +++ b/payment_monetico/data/payment_acquirer_data.xml @@ -0,0 +1,40 @@ + + + + + + + Monetico + + monetico + test + + + + 0000001 + company_code + 12345678901234567890123456789012345678P0 + + +

Accept payments with Monetico secure payment gateway.

+
    +
  • Online Payment
  • +
  • eCommerce
  • +
+
+
+ +
+
diff --git a/payment_monetico/models/__init__.py b/payment_monetico/models/__init__.py new file mode 100644 index 000000000..f65284264 --- /dev/null +++ b/payment_monetico/models/__init__.py @@ -0,0 +1 @@ +from . import payment diff --git a/payment_monetico/models/payment.py b/payment_monetico/models/payment.py new file mode 100644 index 000000000..71d798522 --- /dev/null +++ b/payment_monetico/models/payment.py @@ -0,0 +1,269 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +import json +import logging +from base64 import b64encode +from datetime import datetime +from encodings import hex_codec +from hashlib import sha1 +from hmac import HMAC + +import pytz +from werkzeug import urls + +from odoo import api, fields, models +from odoo.tools.float_utils import float_compare +from odoo.tools.translate import _ + +from odoo.addons.payment.models.payment_acquirer import ValidationError + +from ..controllers.main import MoneticoController + +_logger = logging.getLogger(__name__) + +OUT_DATE_FORMAT = "%d/%m/%Y:%H:%M:%S" +IN_DATE_FORMAT = "%d/%m/%Y_a_%H:%M:%S" + + +class AcquirerMonetico(models.Model): + _inherit = "payment.acquirer" + + provider = fields.Selection( + selection_add=[("monetico", "Monetico")], ondelete={"monetico": "set default"} + ) + monetico_ept = fields.Char( + "EPT number", required_if_provider="monetico", groups="base.group_user" + ) + monetico_company_code = fields.Char( + "Company code", + required_if_provider="monetico", + ) + monetico_secret = fields.Char( + "Secret Key", required_if_provider="monetico", groups="base.group_user" + ) + monetico_test_url = fields.Char( + "Test url", + required_if_provider="monetico", + default="https://p.monetico-services.com/test/paiement.cgi", + ) + monetico_prod_url = fields.Char( + "Production url", + required_if_provider="monetico", + default="https://p.monetico-services.com/paiement.cgi", + ) + monetico_version = fields.Char( + "Interface Version", required_if_provider="monetico", default="3.0" + ) + + def _get_monetico_sign_key(self): + key = self.monetico_secret + hexStrKey = key[0:38] + hexFinal = key[38:40] + "00" + + cca0 = ord(hexFinal[0:1]) + + if cca0 > 70 and cca0 < 97: + hexStrKey += chr(cca0 - 23) + hexFinal[1:2] + elif hexFinal[1:2] == "M": + hexStrKey += hexFinal[0:1] + "0" + else: + hexStrKey += hexFinal[0:2] + + c = hex_codec.Codec() + hexStrKey = c.decode(hexStrKey)[0] + + return hexStrKey + + def _get_monetico_contexte_commande(self, values): + billing_country = values.get("billing_partner_country") + billing_country = billing_country.code if billing_country else None + billing_state = values.get("billing_partner_state") + billing_state = billing_state.code if billing_state else None + + billing = dict( + firstName=values.get("billing_partner_first_name"), + lastName=values.get("billing_partner_last_name"), + mobilePhone=values.get("billing_partner_phone"), + addressLine1=values.get("billing_partner_address"), + city=values.get("billing_partner_city"), + postalCode=values.get("billing_partner_zip"), + country=billing_country, + email=values.get("billing_partner_email"), + stateOrProvince=billing_state, + ) + + # shipping = dict( + # firstName="Ada", + # lastName="Lovelace", + # addressLine1="101 Rue de Roisel", + # city="Y", + # postalCode="80190", + # country="FR", + # email="ada@some.tld", + # phone="+33-612345678", + # shipIndicator="billing_address", + # deliveryTimeframe="two_day", + # firstUseDate="2017-01-25", + # matchBillingAddress=True, + # ) + + # client = dict( + # email="ada@some.tld", + # birthCity="Londre", + # birthPostalCode="W1", + # birthCountry="GB", + # birthdate="2000-12-10", + # ) + + return dict(billing=billing) + + def _monetico_generate_shasign(self, values): + """Generate the shasign for incoming or outgoing communications. + :param dict values: transaction values + :return string: shasign + """ + if self.provider != "monetico": + raise ValidationError(_("Incorrect payment acquirer provider")) + signed_items = dict(sorted(values.items())) + signed_items.pop("MAC", None) + signed_str = "*".join(f"{k}={v}" for k, v in signed_items.items()) + + hmac = HMAC(self._get_monetico_sign_key(), None, sha1) + hmac.update(signed_str.encode("iso8859-1")) + + return hmac.hexdigest() + + def _monetico_form_presign_hook(self, values): + return values + + def monetico_form_generate_values(self, values): + self.ensure_one() + base_url = self.get_base_url() + currency = self.env["res.currency"].sudo().browse(values["currency_id"]) + amount = f"{values['amount']:.2f}{currency.name}" + + lang = values.get("partner_lang") + if lang: + lang = lang.split("_")[0].upper() + + if lang not in ["DE", "EN", "ES", "FR", "IT", "JA", "NL", "PT", "SV"]: + lang = "FR" + + monetico_tx_values = dict( + TPE=self.monetico_ept, + contexte_commande=b64encode( + json.dumps(self._get_monetico_contexte_commande(values)).encode("utf-8") + ).decode("utf-8"), + date=fields.Datetime.now().strftime(OUT_DATE_FORMAT), + lgue=lang, + mail=values.get("partner_email"), + montant=amount, + reference=values["reference"], + societe=self.monetico_company_code, + url_retour_ok=urls.url_join(base_url, MoneticoController._return_url), + url_retour_err=urls.url_join(base_url, MoneticoController._return_url), + version=self.monetico_version, + ) + + monetico_tx_values = self._monetico_form_presign_hook(monetico_tx_values) + + shasign = self._monetico_generate_shasign(monetico_tx_values) + monetico_tx_values["MAC"] = shasign + return monetico_tx_values + + def monetico_get_form_action_url(self): + self.ensure_one() + return ( + self.monetico_prod_url + if self.state == "enabled" + else self.monetico_test_url + ) + + +class TxMonetico(models.Model): + _inherit = "payment.transaction" + + _monetico_valid_tx_status = ["paiement", "payetest"] + _monetico_refused_tx_status = ["annulation"] + + @api.model + def _monetico_form_get_tx_from_data(self, data): + """Given a data dict coming from monetico, verify it and find the related + transaction record.""" + values = dict(data) + shasign = values.pop("MAC", False) + if not shasign: + raise ValidationError(_("Monetico: received data with missing MAC")) + + tx = self.search([("reference", "=", values.get("reference"))]) + if not tx: + error_msg = _( + "Monetico: received data for reference %s; no order found" + ) % values.get("reference") + _logger.error(error_msg) + raise ValidationError(error_msg) + + if shasign.upper() != tx.acquirer_id._monetico_generate_shasign(data).upper(): + + raise ValidationError(_("Monetico: invalid shasign")) + + return tx + + def _monetico_form_get_invalid_parameters(self, data): + invalid_parameters = [] + + amount = data.get("montant", data.get("montantestime")) + # currency and amount should match + amount, currency = amount[:-3], amount[-3:] + if currency != self.currency_id.name: + invalid_parameters.append(("currency", currency, self.currency_id.name)) + + if float_compare(float(amount), self.amount, 2) != 0: + invalid_parameters.append(("amount", amount, "%.2f" % self.amount)) + + return invalid_parameters + + def _monetico_form_validate(self, data): + status = data.get("code-retour").lower() + date = data.get("date") + if date: + try: + date = ( + datetime.strptime(date, IN_DATE_FORMAT) + .astimezone(pytz.utc) + .replace(tzinfo=None) + ) + except Exception: + date = fields.Datetime.now() + data = { + "acquirer_reference": data.get("reference"), + "date": date, + } + + # TODO: add html_3ds status from authentification param + + res = False + if status in self._monetico_valid_tx_status: + msg = f"ref: {self.reference}, got valid response [{status}], set as done." + _logger.info(msg) + data.update(state_message=msg) + self.write(data) + self._set_transaction_done() + self.execute_callback() + res = True + elif status in self._monetico_refused_tx_status: + msg = f"ref: {self.reference}, got refused response [{status}], set as cancel." + data.update(state_message=msg) + self.write(data) + self._set_transaction_cancel() + else: + msg = f"ref: {self.reference}, got unrecognized response [{status}], set as cancel." + data.update(state_message=msg) + self.write(data) + self._set_transaction_cancel() + + _logger.info(msg) + return res diff --git a/payment_monetico/readme/CONTRIBUTORS.rst b/payment_monetico/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..a4d0ad922 --- /dev/null +++ b/payment_monetico/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `Akretion `_: + + * Florian Mounier diff --git a/payment_monetico/readme/DESCRIPTION.rst b/payment_monetico/readme/DESCRIPTION.rst new file mode 100644 index 000000000..8ae28ec21 --- /dev/null +++ b/payment_monetico/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +Payment Acquirer for `Monetico `_ french payment gateway. + diff --git a/payment_monetico/static/description/index.html b/payment_monetico/static/description/index.html new file mode 100644 index 000000000..de144b4fd --- /dev/null +++ b/payment_monetico/static/description/index.html @@ -0,0 +1,423 @@ + + + + + +Monetico Payment Acquirer + + + +
+

Monetico Payment Acquirer

+ + +

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

+

Payment Acquirer for Monetico french payment gateway.

+

Table of contents

+ +
+

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

+
    +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

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-france project on GitHub.

+

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

+
+
+
+ + diff --git a/payment_monetico/static/src/img/logo.png b/payment_monetico/static/src/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1d0a1312f69ef754bb2935fcae5bdafb63b3d289 GIT binary patch literal 3559 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 literal 0 HcmV?d00001 diff --git a/payment_monetico/tests/__init__.py b/payment_monetico/tests/__init__.py new file mode 100644 index 000000000..6a8339889 --- /dev/null +++ b/payment_monetico/tests/__init__.py @@ -0,0 +1 @@ +from . import test_monetico diff --git a/payment_monetico/tests/test_monetico.py b/payment_monetico/tests/test_monetico.py new file mode 100644 index 000000000..043555330 --- /dev/null +++ b/payment_monetico/tests/test_monetico.py @@ -0,0 +1,176 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import json +from base64 import b64encode +from urllib.parse import parse_qs + +from freezegun import freeze_time + +from odoo.tests import tagged + +from odoo.addons.payment.tests.common import PaymentAcquirerCommon + + +@tagged("post_install", "-at_install", "-standard", "external") +class TestPaymentMonetico(PaymentAcquirerCommon): + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + cls.monetico = cls.env.ref("payment_monetico.payment_acquirer_monetico") + cls.monetico.write( + { + "state": "test", + "monetico_ept": "1234567", + "monetico_company_code": "company_1", + "monetico_secret": "12345678901234567890123456789012345678P0", + } + ) + + @classmethod + def _from_qs(cls, qs): + return {k: v[0] for k, v in parse_qs(qs).items()} + + @freeze_time("2024-04-09 14:08:37") + def test_monetico_form_render(self): + self.assertEqual(self.monetico.state, "test", "test without test environment") + + self.env["payment.transaction"].create( + { + "acquirer_id": self.monetico.id, + "amount": 100.0, + "reference": "SO404", + "currency_id": self.currency_euro.id, + "partner_country_id": self.country_france.id, + } + ) + order_ctx = b64encode( + json.dumps( + dict( + billing=dict( + firstName="Norbert", + lastName="Buyer", + mobilePhone="0032 12 34 56 78", + addressLine1="Huge Street 2/543", + city="Sin City", + postalCode="1000", + country="BE", + email="norbert.buyer@example.com", + stateOrProvince=None, + ) + ) + ).encode("utf-8") + ).decode("utf-8") + + self.assertEqual( + self.monetico.render( + "SO404", 100.0, self.currency_euro.id, values=self.buyer_values + ).decode("utf-8"), + """ + + + + + + + + + + + + + + """ # noqa + % order_ctx, + ) + + def test_monetico_form_management_success(self): + self.assertEqual(self.monetico.state, "test", "test without test environment") + # Monetico sample post data + monetico_post_data = self._from_qs( + "TPE=1234567" + "&date=05%2f12%2f2006%5fa%5f11%3a55%3a23" + "&montant=62%2e75EUR" + "&reference=SO100x1" + "&MAC=A384F76DBD3A59B2F7B019F3574589217CAFB2CE" + "&texte-libre=LeTexteLibre" + "&code-retour=paiement" + "&cvx=oui" + "&vld=1208" + "&brand=VI" + "&status3ds=1" + "&numauto=010101" + "&originecb=FRA" + "&bincb=12345678" + "&hpancb=74E94B03C22D786E0F2C2CADBFC1C00B004B7C45" + "&ipclient=127%2e0%2e0%2e1" + "&originetr=FRA" + "&modepaiement=CB" + "&veres=Y" + "&pares=Y" + ) + + tx = self.env["payment.transaction"].create( + { + "amount": 62.75, + "acquirer_id": self.monetico.id, + "currency_id": self.currency_euro.id, + "reference": "SO100x1", + "partner_name": "Norbert Buyer", + "partner_country_id": self.country_france.id, + } + ) + + tx.form_feedback(monetico_post_data, "monetico") + self.assertEqual( + tx.state, "done", "Monetico: validation did not put tx into done state" + ) + self.assertEqual( + tx.acquirer_reference, + "SO100x1", + "Monetico: validation did not update tx id", + ) + + def test_monetico_form_management_error(self): + self.assertEqual(self.monetico.state, "test", "test without test environment") + # Monetico sample post data + monetico_post_data = self._from_qs( + "TPE=9000001" + "&date=05%2f10%2f2011%5fa%5f15%3a33%3a06" + "&montant=1%2e01EUR" + "&reference=SO100x2" + "&MAC=DE96CB30E9239E2D5AE03063799C9B76F3F9FA60" + "&textelibre=Ceci+est+un+test%2c+ne+pas+tenir+compte%2e" + "&code-retour=Annulation" + "&cvx=oui" + "&vld=0912" + "&brand=MC" + "&status3ds=-1" + "&motifrefus=filtrage" + "&originecb=FRA" + "&bincb=12345678" + "&hpancb=764AD24CFABBB818E8A7DC61D4D6B4B89EA837ED" + "&ipclient=10%2e45%2e166%2e76" + "&originetr=inconnue" + "&modepaiement=CB" + "&veres=" + "&pares=" + "&filtragecause=4-" + "&filtragevaleur=FRA-" + ) + tx = self.env["payment.transaction"].create( + { + "amount": 1.01, + "acquirer_id": self.monetico.id, + "currency_id": self.currency_euro.id, + "reference": "SO100x2", + "partner_name": "Norbert Buyer", + "partner_country_id": self.country_france.id, + } + ) + tx.form_feedback(monetico_post_data, "monetico") + self.assertEqual( + tx.state, + "cancel", + ) diff --git a/payment_monetico/views/payment_monetico_templates.xml b/payment_monetico/views/payment_monetico_templates.xml new file mode 100644 index 000000000..e6451230a --- /dev/null +++ b/payment_monetico/views/payment_monetico_templates.xml @@ -0,0 +1,34 @@ + + + + + + + diff --git a/payment_monetico/views/payment_views.xml b/payment_monetico/views/payment_views.xml new file mode 100644 index 000000000..72d4eb4d1 --- /dev/null +++ b/payment_monetico/views/payment_views.xml @@ -0,0 +1,49 @@ + + + + + + + acquirer.form.monetico + payment.acquirer + + + + + + + + + + + + + + + + + diff --git a/setup/payment_monetico/odoo/addons/payment_monetico b/setup/payment_monetico/odoo/addons/payment_monetico new file mode 120000 index 000000000..03ac706c7 --- /dev/null +++ b/setup/payment_monetico/odoo/addons/payment_monetico @@ -0,0 +1 @@ +../../../../payment_monetico \ No newline at end of file diff --git a/setup/payment_monetico/setup.py b/setup/payment_monetico/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/payment_monetico/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 36571c554c83b036f0b22882bb7a428e875c35de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Wed, 31 Jul 2024 20:13:29 +0200 Subject: [PATCH 2/2] HACK: mobie phone shoud be formated correctly --- payment_monetico/models/payment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/payment_monetico/models/payment.py b/payment_monetico/models/payment.py index 71d798522..c13eddac5 100644 --- a/payment_monetico/models/payment.py +++ b/payment_monetico/models/payment.py @@ -86,7 +86,7 @@ def _get_monetico_contexte_commande(self, values): billing = dict( firstName=values.get("billing_partner_first_name"), lastName=values.get("billing_partner_last_name"), - mobilePhone=values.get("billing_partner_phone"), + mobilePhone=None, # TODO Format correctly values.get("billing_partner_phone"), addressLine1=values.get("billing_partner_address"), city=values.get("billing_partner_city"), postalCode=values.get("billing_partner_zip"),