From c75cc8289905ea113b452cf55bcb6afd0ce4b2b3 Mon Sep 17 00:00:00 2001 From: Romain <73525560+Rom10811@users.noreply.github.com> Date: Wed, 6 Dec 2023 15:11:56 +0100 Subject: [PATCH 1/2] [MIG] l10n_fr_payment_payfip --- l10n_fr_payment_payfip/.gitignore | 206 +++++++++ l10n_fr_payment_payfip/CHANGELOG.md | 6 + l10n_fr_payment_payfip/README.md | 34 ++ l10n_fr_payment_payfip/__init__.py | 12 + l10n_fr_payment_payfip/__manifest__.py | 28 ++ .../controllers/__init__.py | 1 + l10n_fr_payment_payfip/controllers/main.py | 66 +++ .../data/payment_provider.xml | 31 ++ .../data/payment_provider_data.xml | 20 + l10n_fr_payment_payfip/i18n/fr.po | 227 ++++++++++ l10n_fr_payment_payfip/models/__init__.py | 3 + .../models/account_payment_method.py | 14 + .../models/payment_provider.py | 397 ++++++++++++++++++ .../models/payment_transaction.py | 160 +++++++ l10n_fr_payment_payfip/setup.py | 9 + .../.setuptools-odoo-make-default-ignore | 2 + l10n_fr_payment_payfip/setup/README | 2 + .../payment_payfip/odoo/addons/payment_payfip | 1 + .../setup/payment_payfip/setup.cfg | 2 + .../setup/payment_payfip/setup.py | 6 + .../static/description/icon.png | Bin 0 -> 4429 bytes .../static/src/img/payfip_icon.png | Bin 0 -> 4429 bytes l10n_fr_payment_payfip/tests/__init__.py | 4 + l10n_fr_payment_payfip/tests/common.py | 18 + l10n_fr_payment_payfip/tests/test_payfip.py | 155 +++++++ .../views/payment_payfip_templates.xml | 16 + .../views/payment_views.xml | 56 +++ requirements.txt | 2 + .../odoo/addons/l10n_fr_payment_payfip | 1 + setup/l10n_fr_payment_payfip/setup.py | 6 + 30 files changed, 1485 insertions(+) create mode 100644 l10n_fr_payment_payfip/.gitignore create mode 100644 l10n_fr_payment_payfip/CHANGELOG.md create mode 100644 l10n_fr_payment_payfip/README.md create mode 100644 l10n_fr_payment_payfip/__init__.py create mode 100644 l10n_fr_payment_payfip/__manifest__.py create mode 100644 l10n_fr_payment_payfip/controllers/__init__.py create mode 100644 l10n_fr_payment_payfip/controllers/main.py create mode 100644 l10n_fr_payment_payfip/data/payment_provider.xml create mode 100644 l10n_fr_payment_payfip/data/payment_provider_data.xml create mode 100644 l10n_fr_payment_payfip/i18n/fr.po create mode 100644 l10n_fr_payment_payfip/models/__init__.py create mode 100644 l10n_fr_payment_payfip/models/account_payment_method.py create mode 100644 l10n_fr_payment_payfip/models/payment_provider.py create mode 100644 l10n_fr_payment_payfip/models/payment_transaction.py create mode 100644 l10n_fr_payment_payfip/setup.py create mode 100644 l10n_fr_payment_payfip/setup/.setuptools-odoo-make-default-ignore create mode 100644 l10n_fr_payment_payfip/setup/README create mode 100644 l10n_fr_payment_payfip/setup/payment_payfip/odoo/addons/payment_payfip create mode 100644 l10n_fr_payment_payfip/setup/payment_payfip/setup.cfg create mode 100644 l10n_fr_payment_payfip/setup/payment_payfip/setup.py create mode 100644 l10n_fr_payment_payfip/static/description/icon.png create mode 100644 l10n_fr_payment_payfip/static/src/img/payfip_icon.png create mode 100644 l10n_fr_payment_payfip/tests/__init__.py create mode 100644 l10n_fr_payment_payfip/tests/common.py create mode 100644 l10n_fr_payment_payfip/tests/test_payfip.py create mode 100644 l10n_fr_payment_payfip/views/payment_payfip_templates.xml create mode 100644 l10n_fr_payment_payfip/views/payment_views.xml create mode 120000 setup/l10n_fr_payment_payfip/odoo/addons/l10n_fr_payment_payfip create mode 100644 setup/l10n_fr_payment_payfip/setup.py diff --git a/l10n_fr_payment_payfip/.gitignore b/l10n_fr_payment_payfip/.gitignore new file mode 100644 index 000000000..a1061d116 --- /dev/null +++ b/l10n_fr_payment_payfip/.gitignore @@ -0,0 +1,206 @@ + +# Created by https://www.gitignore.io/api/python,pycharm +# Edit at https://www.gitignore.io/?templates=python,pycharm + +### PyCharm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### PyCharm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +.idea/**/sonarlint/ + +# SonarQube Plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator/ + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# End of https://www.gitignore.io/api/python,pycharm + +# SPECIFIC PYCHARM # + +.idea/ + +#################### + + diff --git a/l10n_fr_payment_payfip/CHANGELOG.md b/l10n_fr_payment_payfip/CHANGELOG.md new file mode 100644 index 000000000..d62607232 --- /dev/null +++ b/l10n_fr_payment_payfip/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog +All notable changes to this project will be documented in this file. + +## [16.0.1] 2024-03-06 +### Initialisation of the module. + diff --git a/l10n_fr_payment_payfip/README.md b/l10n_fr_payment_payfip/README.md new file mode 100644 index 000000000..eec7ee07f --- /dev/null +++ b/l10n_fr_payment_payfip/README.md @@ -0,0 +1,34 @@ +l10n_fr_payment_payfip +========= +This module add an option to use payfip + +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 + +* Moka Tourisme + +## Contributors + +* Horvat Damien : +* Duciel Romain : + + +## Maintainers + +This module is maintained by Moka Tourisme. + + +This module is a addon for the `Odoo/addons/payment `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. \ No newline at end of file diff --git a/l10n_fr_payment_payfip/__init__.py b/l10n_fr_payment_payfip/__init__.py new file mode 100644 index 000000000..0c6a34381 --- /dev/null +++ b/l10n_fr_payment_payfip/__init__.py @@ -0,0 +1,12 @@ +from . import models +from . import controllers + +from odoo.addons.payment import setup_provider, reset_payment_provider + + +def post_init_hook(cr, registry): + setup_provider(cr, registry, 'payfip') + + +def uninstall_hook(cr, registry): + reset_payment_provider(cr, registry, 'payfip') \ No newline at end of file diff --git a/l10n_fr_payment_payfip/__manifest__.py b/l10n_fr_payment_payfip/__manifest__.py new file mode 100644 index 000000000..a48cbe62d --- /dev/null +++ b/l10n_fr_payment_payfip/__manifest__.py @@ -0,0 +1,28 @@ +{ + "name": "Intermédiaire de paiement PayFIP", + "version": "16.0.1.0.1", + "summary": """Intermédiaire de paiement : Implémentation de PayFIP""", + "author": "MokaTourisme," "Odoo Community Association (OCA)", + "website": "https://github.com/OCA/l10n-france", + "license": "AGPL-3", + "category": "Accounting", + "external_dependencies": { + "python": [ + "openupgradelib", + ] + }, + "depends": ["payment", "l10n_fr"], + "qweb": [], + "init_xml": [], + "update_xml": [], + "data": [ + # Views must be before data to avoid loading issues + "views/payment_payfip_templates.xml", + "views/payment_views.xml", + "data/payment_provider_data.xml", + ], + "demo": [], + "application": False, + "auto_install": False, + "installable": True, +} diff --git a/l10n_fr_payment_payfip/controllers/__init__.py b/l10n_fr_payment_payfip/controllers/__init__.py new file mode 100644 index 000000000..12a7e529b --- /dev/null +++ b/l10n_fr_payment_payfip/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/l10n_fr_payment_payfip/controllers/main.py b/l10n_fr_payment_payfip/controllers/main.py new file mode 100644 index 000000000..06b999c71 --- /dev/null +++ b/l10n_fr_payment_payfip/controllers/main.py @@ -0,0 +1,66 @@ +import logging +import pprint +import werkzeug + +from odoo import http +from odoo.http import request + +from odoo.exceptions import ValidationError + + +_logger = logging.getLogger(__name__) + + +class PayFIPController(http.Controller): + _payment_url = '/payment/payfip/pay' + _return_url = '/payment/payfip/dpn' + _notification_url = '/payment/payfip/ipn' + + @http.route(_payment_url, type='http', auth='public', methods=['GET', 'POST'], csrf=False, save_session=False) + def payfip_pay(self, **post): + reference = post.pop('objet', False) + amount = float(post.pop('montant', 0)) + return_url = post.pop('urlredirect', '/payment/status') + tx = request.env['payment.transaction'].sudo().search([('reference', '=', reference), ('amount', '=', amount)]) + if tx and tx.provider_id.code == 'payfip': + # PayFIP doesn't accept two attempts with the same operation identifier, we check if transaction has + # already sent and recreate it in this case. + if tx.payfip_sent_to_webservice: + tx = tx.copy({ + 'reference': request.env['payment.transaction'].get_next_reference(tx.reference), + }) + + tx.write({ + 'payfip_return_url': return_url, + 'payfip_sent_to_webservice': True, + }) + return werkzeug.utils.redirect('{url}?idop={idop}'.format( + url="https://www.tipi.budget.gouv.fr/tpa/paiementws.web", + idop=tx.payfip_operation_identifier, + )) + else: + return werkzeug.utils.redirect('/') + + @http.route(_notification_url, type='http', auth='public', methods=['POST'], csrf=False, save_session=False) + def payfip_ipn(self, **post): + """Process PayFIP IPN.""" + _logger.debug('Beginning PayFIP IPN form_feedback with post data %s', pprint.pformat(post)) + if not post or not post.get('idop'): + raise ValidationError("No idOp found for transaction on PayFIP") + + idop = post.get('idop', False) + tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data('payfip', idop) + tx_sudo._handle_notification_data('payfip', idop) + + return '' + + @http.route(_return_url, type="http", auth="public", methods=["POST", "GET"], csrf=False, save_session=False) + def payfip_dpn(self, **post): + """Process PayFIP DPN.""" + _logger.debug('Beginning PayFIP DPN form_feedback with post data %s', pprint.pformat(post)) + + idop = post.get('idop', False) + tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_notification_data('payfip', idop) + tx_sudo._handle_notification_data('payfip', idop) + + return request.redirect('/payment/status') diff --git a/l10n_fr_payment_payfip/data/payment_provider.xml b/l10n_fr_payment_payfip/data/payment_provider.xml new file mode 100644 index 000000000..1dd006a6a --- /dev/null +++ b/l10n_fr_payment_payfip/data/payment_provider.xml @@ -0,0 +1,31 @@ + + + + + PayFIP + + payfip + + + + You will be redirected to the PayFIP website after clicking on the payment button.

]]> +
+ dummy + +

PayFIP est un système de paiement en ligne français proposé pa la Direction générale des Finances + Publiques. Son but est de faciliter le paiement des services publics locaux. +

+
    +
  • eCommerce +
  • +
+
+
+
+ + + PayFip + payfip + inbound + +
diff --git a/l10n_fr_payment_payfip/data/payment_provider_data.xml b/l10n_fr_payment_payfip/data/payment_provider_data.xml new file mode 100644 index 000000000..9284fe6d8 --- /dev/null +++ b/l10n_fr_payment_payfip/data/payment_provider_data.xml @@ -0,0 +1,20 @@ + + + + PayFIP + Module PayFIP + payfip + + + + dummy + + You will be redirected to the PayFIP website after clicking on the payment button.

]]> +
+
+ + PayFip + payfip + inbound + +
diff --git a/l10n_fr_payment_payfip/i18n/fr.po b/l10n_fr_payment_payfip/i18n/fr.po new file mode 100644 index 000000000..09e2a8b68 --- /dev/null +++ b/l10n_fr_payment_payfip/i18n/fr.po @@ -0,0 +1,227 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * payment_tipiregie +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-03-06 09:48+0000\n" +"PO-Revision-Date: 2024-03-06 09:48+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: payment_tipiregie +#: model:mail.template,body_html:payment_tipiregie.mail_template_draft_payments_recovered +msgid "\n" +"\n" +"% set transactions = ctx and ctx['transactions'] or []\n" +"\n" +"

Some PayFIP payment transactions were in draft state from too long.

\n" +"

\n" +"Take a look at the list and the new state after verification:\n" +"

    \n" +" % for tx in transactions:\n" +"
  • ${tx.reference} from ${tx.create_date} with amount of ${tx.amount} is now ${tx.state}
  • \n" +" %endfor\n" +"
\n" +"

" +msgstr "\n" +"\n" +"% set transactions = ctx and ctx['transactions'] or []\n" +"\n" +"

Des transactions de paiement de PayFIP sont restées dans l'état brouillon trop longtemps.

\n" +"

\n" +"Veuillez regarder la liste et les nouveaux statuts récupérés auprès de PayFIP :\n" +"

    \n" +" % for tx in transactions:\n" +"
  • ${tx.reference} du ${tx.create_date} avec le montant de ${tx.amount} est maintenant ${tx.state}
  • \n" +" %endfor\n" +"
\n" +"

" + +#. module: payment_tipiregie +#: code:addons/payment_tipiregie/models/inherited_payment_acquirer.py:280 +#, python-format +msgid "\n" +"PayFIP server returned the following error: \"%s\"" +msgstr "\n" +"Le serveur PayFIP retourne l'erreur suivante : \"%s\"" + +#. module: payment_tipiregie +#: model:ir.ui.view,arch_db:payment_tipiregie.acquirer_form_tipiregie +msgid "In activation" +msgstr "En activation" + +#. module: payment_tipiregie +#: model:ir.ui.view,arch_db:payment_tipiregie.acquirer_form_tipiregie +msgid "Not in activation" +msgstr "Pas en activation" + +#. module: payment_tipiregie +#: model:payment.acquirer,cancel_msg:payment_tipiregie.payment_acquirer_tipiregie +msgid "Annulé, votre paiement a été annulé." +msgstr "Annulé, votre paiement a été annulé." + +#. module: payment_tipiregie +#: model:payment.acquirer,pending_msg:payment_tipiregie.payment_acquirer_tipiregie +msgid "En attente, Votre paiement en ligne a été enregistré avec succès, mais votre commande n'est pas encore validée." +msgstr "En attente, Votre paiement en ligne a été enregistré avec succès, mais votre commande n'est pas encore validée." + +#. module: payment_tipiregie +#: model:payment.acquirer,error_msg:payment_tipiregie.payment_acquirer_tipiregie +msgid "Erreur, veuillez noter qu'une erreur est survenue durant la transaction. La commande a été confirmée mais ne sera pas payée. N'hésitez pas à nous contacter si vous avez la moindre question sur le statut de votre commande." +msgstr "Erreur, veuillez noter qu'une erreur est survenue durant la transaction. La commande a été confirmée mais ne sera pas payée. N'hésitez pas à nous contacter si vous avez la moindre question sur le statut de votre commande." + +#. module: payment_tipiregie +#: model:payment.acquirer,done_msg:payment_tipiregie.payment_acquirer_tipiregie +msgid "Fait, votre paiement en ligne a été enregistré. Merci de votre commande." +msgstr "Fait, votre paiement en ligne a été enregistré. Merci de votre commande." + +#. module: payment_tipiregie +#: selection:payment.transaction,tipiregie_state:0 +msgid "Abandoned payment (A)" +msgstr "Paiement abandonné (A)" + +#. module: payment_tipiregie +#: model:ir.model.fields,field_description:payment_tipiregie.field_payment_acquirer_tipiregie_activation_mode +msgid "Activation mode" +msgstr "Mode activation" + +#. module: payment_tipiregie +#: model:ir.ui.view,arch_db:payment_tipiregie.transaction_form_tipiregie +msgid "Check PayFIP transaction" +msgstr "Vérifier la transaction PayFIP" + +#. module: payment_tipiregie +#: model:ir.actions.server,name:payment_tipiregie.tipiregie_check_transaction_action_server +msgid "Check PayFIP transactions" +msgstr "Vérifier les transactions PayFIP" + +#. module: payment_tipiregie +#: model:ir.actions.server,name:payment_tipiregie.cron_check_draft_payment_transactions_ir_actions_server +#: model:ir.cron,cron_name:payment_tipiregie.cron_check_draft_payment_transactions +#: model:ir.cron,name:payment_tipiregie.cron_check_draft_payment_transactions +msgid "Cron to check PayFIP draft payment transactions" +msgstr "Cron pour vérifier les transactions de paiement PayFIP en brouillon" + +#. module: payment_tipiregie +#: model:ir.model.fields,field_description:payment_tipiregie.field_payment_acquirer_tipiregie_customer_number +msgid "Customer number" +msgstr "Numéro client" + +#. module: payment_tipiregie +#: selection:payment.transaction,tipiregie_state:0 +msgid "Effective payment (P)" +msgstr "Paiement effectif (P)" + +#. module: payment_tipiregie +#: selection:payment.transaction,tipiregie_state:0 +msgid "Effective payment (V)" +msgstr "Paiement effectif (V)" + +#. module: payment_tipiregie +#: model:ir.model.fields,field_description:payment_tipiregie.field_payment_acquirer_tipiregie_form_action_url +msgid "Form action URL" +msgstr "URL de renvoi du formulaire" + +#. module: payment_tipiregie +#: code:addons/payment_tipiregie/models/inherited_payment_acquirer.py:247 +#, python-format +msgid "It would appear that the customer number entered is not valid or that the PayFIP contract is not properly configured." +msgstr "Il semblerait que le numéro client n'est pas valide ou que le contrat PayFIP n'est pas correctement configuré." + +#. module: payment_tipiregie +#: model:ir.model.fields,field_description:payment_tipiregie.field_payment_transaction_tipiregie_operation_identifier +msgid "Operation identifier" +msgstr "Identifiant d'opération" + +#. module: payment_tipiregie +#: selection:payment.transaction,tipiregie_state:0 +msgid "Other cases (R)" +msgstr "Autres cas (R)" + +#. module: payment_tipiregie +#: selection:payment.transaction,tipiregie_state:0 +msgid "Other cases (Z)" +msgstr "Autres cas (Z)" + +#. module: payment_tipiregie +#: model:ir.ui.view,arch_db:payment_tipiregie.transaction_form_tipiregie +#: model:payment.acquirer,name:payment_tipiregie.payment_acquirer_tipiregie +msgid "PayFIP" +msgstr "PayFIP" + +#. module: payment_tipiregie +#: model:mail.template,subject:payment_tipiregie.mail_template_draft_payments_recovered +msgid "PayFIP: Draft payments recovered" +msgstr "PayFIP : Paiements en brouillons réévalués" + +#. module: payment_tipiregie +#: code:addons/payment_tipiregie/models/inherited_payment_acquirer.py:63 +#, python-format +msgid "PayFIP: activation mode can be activate in test environment only and if the payment acquirer is published on the website." +msgstr "PayFIP : l'activation ne peut être activée qu'en environement de test et si l'intermédiaire de paiement est publié sur le site web." + +#. module: payment_tipiregie +#: code:addons/payment_tipiregie/models/inherited_payment_acquirer.py:63 +#, python-format +msgid "PayFIP: activation mode can be activate in test environment only and if the payment acquirer is published on the website." +msgstr "PayFIP : l'activation ne peut être activée qu'en environement de test et si l'intermédiaire de paiement est publié sur le site web." + +#. module: payment_tipiregie +#: code:addons/payment_tipiregie/models/inherited_payment_transaction.py:94 +#: code:addons/payment_tipiregie/models/inherited_payment_transaction.py:119 +#, python-format +msgid "PayFIP: received data with missing idop!" +msgstr "PayFIP : données reçues mais idop manquant !" + +#. module: payment_tipiregie +#: model:ir.model.fields,field_description:payment_tipiregie.field_payment_transaction_tipiregie_amount +msgid "PayFIP amount" +msgstr "Montant PayFIP" + +#. module: payment_tipiregie +#: model:ir.model.fields,field_description:payment_tipiregie.field_payment_transaction_tipiregie_state +msgid "PayFIP state" +msgstr "État PayFIP" + +#. module: payment_tipiregie +#: model:ir.model,name:payment_tipiregie.model_payment_acquirer +msgid "Payment Acquirer" +msgstr "Intermédiaire de Paiement" + +#. module: payment_tipiregie +#: model:ir.model,name:payment_tipiregie.model_payment_transaction +msgid "Payment Transaction" +msgstr "Transaction de paiement" + +#. module: payment_tipiregie +#: model:ir.model.fields,help:payment_tipiregie.field_payment_transaction_tipiregie_operation_identifier +msgid "Reference of the request of TX as stored in the acquirer database" +msgstr "Référence de la demande de transaction telle que stockée dans la base de donnée de l'acquéreur." + +#. module: payment_tipiregie +#: model:ir.model.fields,field_description:payment_tipiregie.field_payment_transaction_tipiregie_return_url +msgid "Return URL" +msgstr "URL de retour" + +#. module: payment_tipiregie +#: model:ir.model.fields,field_description:payment_tipiregie.field_payment_transaction_tipiregie_sent_to_webservice +msgid "Sent to PayFIP webservice" +msgstr "Envoyée au service web de PayFIP" + +#. module: payment_tipiregie +#: selection:payment.transaction,tipiregie_state:0 +msgid "Unknown" +msgstr "Inconnu" + +#. module: payment_tipiregie +#: model:payment.acquirer,pre_msg:payment_tipiregie.payment_acquirer_tipiregie +msgid "You will be redirected to the PayFIP website after clicking on the payment button." +msgstr "Vous allez être redirigé vers le site internet de PayFIP après avoir cliqué sur le bouton de paiement." + diff --git a/l10n_fr_payment_payfip/models/__init__.py b/l10n_fr_payment_payfip/models/__init__.py new file mode 100644 index 000000000..c95729831 --- /dev/null +++ b/l10n_fr_payment_payfip/models/__init__.py @@ -0,0 +1,3 @@ +from . import payment_provider +from . import payment_transaction +from . import account_payment_method diff --git a/l10n_fr_payment_payfip/models/account_payment_method.py b/l10n_fr_payment_payfip/models/account_payment_method.py new file mode 100644 index 000000000..e2452e454 --- /dev/null +++ b/l10n_fr_payment_payfip/models/account_payment_method.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models + + +class AccountPaymentMethod(models.Model): + _inherit = 'account.payment.method' + + @api.model + def _get_payment_method_information(self): + res = super()._get_payment_method_information() + res['payfip'] = {'mode': 'unique', 'domain': [('type', '=', 'bank')]} + return res diff --git a/l10n_fr_payment_payfip/models/payment_provider.py b/l10n_fr_payment_payfip/models/payment_provider.py new file mode 100644 index 000000000..bb6dfeb0d --- /dev/null +++ b/l10n_fr_payment_payfip/models/payment_provider.py @@ -0,0 +1,397 @@ +import logging +import logging +import requests +import urllib.parse +from requests.exceptions import ConnectionError +from odoo.exceptions import ValidationError +from xml.etree import ElementTree +from odoo.http import request + +from odoo import api, fields, models, _ +from odoo.osv import expression + +from ..controllers.main import PayFIPController +from odoo.addons.payment_paypal.const import SUPPORTED_CURRENCIES + +_logger = logging.getLogger(__name__) + + +class PayFIPProvider(models.Model): + # region Private attributes + _inherit = 'payment.provider' + # endregion + + # region Default methods + # endregion + + # region Fields declaration + code = fields.Selection(selection_add=[('payfip', 'PayFIP')], ondelete={ + 'payfip': 'set default'}) + + state = fields.Selection(selection_add=[('activation', 'Activation')], ondelete={ + 'activation': 'set default'}) + + journal_id = fields.Many2one('account.journal', store=True) + + payfip_customer_number = fields.Char( + string="Customer number", + required_if_provider='payfip', + ) + + payfip_base_url = fields.Char( + string="Base URL", + required_if_provider='payfip', + ) + + payfip_notification_url = fields.Char( + string="Notification URL", + required_if_provider='payfip', + help="URL to which PayFIP will send the IPN notifications. like '/payment/payfip/ipn'" + ) + + payfip_redirect_url = fields.Char( + string="Redirect URL", + required_if_provider='payfip', + help="URL to which PayFIP will redirect the user after payment. like '/payment/payfip/dpn'" + ) + + payfip_activation_mode = fields.Boolean( + string="Activation mode", + default=False, + ) + + # endregion + + # region Fields method + # endregion + + # region Constrains and Onchange + @api.constrains('payfip_customer_number') + def _check_payfip_customer_number(self): + self.ensure_one() + if self.code == 'payfip' and self.payfip_customer_number not in ['dummy', '']: + webservice_enabled, message = self._payfip_check_web_service() + if not webservice_enabled: + raise ValidationError(message) + else: + return True + + # endregion + + # region CRUD (overrides) + # endregion + @api.model + def _get_compatible_providers( + self, company_id, partner_id, amount, currency_id=None, force_tokenization=False, + is_express_checkout=False, is_validation=False, **kwargs + ): + # Compute the base domain for compatible providers. + domain = ['&', ('state', 'in', ['enabled', 'test', 'activation']), ('company_id', '=', company_id)] + + # Handle the is_published state. + if not self.env.user._is_internal(): + domain = expression.AND([domain, [('is_published', '=', True)]]) + + # Handle partner country. + partner = self.env['res.partner'].browse(partner_id) + if partner.country_id: # The partner country must either not be set or be supported. + domain = expression.AND([ + domain, [ + '|', + ('available_country_ids', '=', False), + ('available_country_ids', 'in', [partner.country_id.id]), + ] + ]) + + # Handle the maximum amount. + currency = self.env['res.currency'].browse(currency_id).exists() + if not is_validation and currency: # The currency is required to convert the amount. + company = self.env['res.company'].browse(company_id).exists() + date = fields.Date.context_today(self) + converted_amount = currency._convert(amount, company.currency_id, company, date) + domain = expression.AND([ + domain, [ + '|', '|', + ('maximum_amount', '>=', converted_amount), + ('maximum_amount', '=', False), + ('maximum_amount', '=', 0.), + ] + ]) + + # Handle tokenization support requirements. + if force_tokenization or self._is_tokenization_required(**kwargs): + domain = expression.AND([domain, [('allow_tokenization', '=', True)]]) + + # Handle express checkout. + if is_express_checkout: + domain = expression.AND([domain, [('allow_express_checkout', '=', True)]]) + + compatible_providers = self.env['payment.provider'].search(domain) + return compatible_providers + + # region Actions + # endregion + + # region Model methods + def _get_soap_url(self): + return "https://www.tipi.budget.gouv.fr/tpa/services/securite" + + def _get_soap_namespaces(self): + return { + 'ns1': "http://securite.service.tpa.cp.finances.gouv.fr/services/mas_securite/" + "contrat_paiement_securise/PaiementSecuriseService" + } + + def payfip_get_form_action_url(self): + self.ensure_one() + return '/payment/payfip/pay' + + def payfip_get_id_op_from_web_service(self, email, price, object, provider_reference): + self.ensure_one() + id_op = '' + base_url = self.env['ir.config_parameter'].get_param('web.base.url') + if self.state == 'enabled': + saisie_value = 'W' + elif self.state == 'activation': + saisie_value = 'X' + else: + saisie_value = 'T' + + exer = fields.Datetime.now().year + numcli = self.payfip_customer_number + saisie = saisie_value + urlnotif = self.payfip_notification_url + urlredirect = self.payfip_redirect_url + + soap_body = '' + soap_body += """ + + + + + %s + %s + %s + %s + %s + %s + %s + %s + %s + + + + + """ % (exer, email, price, numcli, object, provider_reference, saisie, urlnotif, urlredirect) + try: + response = requests.post(self._get_soap_url(), data=soap_body, headers={ + 'content-type': 'text/xml'}) + except ConnectionError: + return id_op + + root = ElementTree.fromstring(response.content) + errors = self._get_errors_from_webservice(root) + + for error in errors: + _logger.error( + "An error occured during idOp negociation with PayFIP web service. Informations are: {" + "code: %s, description: %s, label: %s, severity: %s}" % ( + error.get('code'), + error.get('description'), + error.get('label'), + error.get('severity'), + ) + ) + return id_op + + idop_element = root.find('.//idOp') + id_op = idop_element.text if idop_element is not None else '' + return id_op + + def payfip_get_result_from_web_service(self, idOp): + data = {} + soap_url = self._get_soap_url() + soap_body = '' + soap_body += """ + + + + + %s + + + + + """ % idOp + + try: + soap_response = requests.post(soap_url, data=soap_body, headers={ + 'content-type': 'text/xml'}) + except ConnectionError: + return data + + root = ElementTree.fromstring(soap_response.content) + errors = self._get_errors_from_webservice(root) + for error in errors: + _logger.error( + "An error occured during idOp negociation with PayFIP web service. Informations are: {" + "code: %s, description: %s, label: %s, severity: %s}" % ( + error.get('code'), + error.get('description'), + error.get('label'), + error.get('severity'), + ) + ) + data = { + 'code': error.get('code'), + } + return data + + response = root.find('.//return') + if response is None: + raise Exception( + "No result found for transaction with idOp: %s" % idOp) + + resultrans = response.find('resultrans') + if resultrans is None: + raise Exception( + "No result found for transaction with idOp: %s" % idOp) + + dattrans = response.find('dattrans') + heurtrans = response.find('heurtrans') + exer = response.find('exer') + idOp = response.find('idOp') + mel = response.find('mel') + montant = response.find('montant') + numcli = response.find('numcli') + objet = response.find('objet') + refdet = response.find('refdet') + saisie = response.find('saisie') + + data = { + 'resultrans': resultrans.text if resultrans is not None else False, + 'dattrans': dattrans.text if dattrans is not None else False, + 'heurtrans': heurtrans.text if heurtrans is not None else False, + 'exer': exer.text if exer is not None else False, + 'idOp': idOp.text if idOp is not None else False, + 'mel': mel.text if mel is not None else False, + 'montant': montant.text if montant is not None else False, + 'numcli': numcli.text if numcli is not None else False, + 'objet': objet.text if objet is not None else False, + 'refdet': refdet.text if refdet is not None else False, + 'saisie': saisie.text if saisie is not None else False, + } + return data + + def _payfip_check_web_service(self): + self.ensure_one() + error = _("It would appear that the customer number entered is not valid or that the PayFIP contract is " + "not properly configured.") + + soap_url = self._get_soap_url() + soap_body = """ + + + + + + %s + + + + + """ % ( + 'xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"', + 'xmlns:pai="http://securite.service.tpa.cp.finances.gouv.fr/services/mas_securite/' + 'contrat_paiement_securise/PaiementSecuriseService"', + self.payfip_customer_number + ) + + try: + soap_response = requests.post(soap_url, data=soap_body, headers={ + 'content-type': 'text/xml'}) + + except ConnectionError: + return False, error + + root = ElementTree.fromstring(soap_response.content) + fault = root.find( + './/S:Fault', {'S': 'http://schemas.xmlsoap.org/soap/envelope/'}) + + if fault is not None: + error_desc = fault.find('.//descriptif') + if error_desc is not None: + error += _("\nPayFIP server returned the following error: \"%s\"") % error_desc.text + return False, error + + return True, '' + + def _get_errors_from_webservice(self, root): + errors = [] + + namespaces = self._get_soap_namespaces() + error_functionnal = root.find('.//ns1:FonctionnelleErreur', namespaces) + error_dysfonctionnal = root.find( + './/ns1:TechDysfonctionnementErreur', namespaces) + error_unavailabilityl = root.find( + './/ns1:TechIndisponibiliteErreur', namespaces) + error_protocol = root.find('.//ns1:TechProtocolaireErreur', namespaces) + + if error_functionnal is not None: + code = error_functionnal.find('code') + label = error_functionnal.find('libelle') + description = error_functionnal.find('descriptif') + severity = error_functionnal.find('severite') + errors += [{ + 'code': code.text if code is not None else 'NC', + 'label': label.text if label is not None else 'NC', + 'description': description.text if description is not None else 'NC', + 'severity': severity.text if severity is not None else 'NC', + }] + if error_dysfonctionnal is not None: + code = error_dysfonctionnal.find('code') + label = error_dysfonctionnal.find('libelle') + description = error_dysfonctionnal.find('descriptif') + severity = error_dysfonctionnal.find('severite') + errors += [{ + 'code': code.text if code is not None else 'NC', + 'label': label.text if label is not None else 'NC', + 'description': description.text if description is not None else 'NC', + 'severity': severity.text if severity is not None else 'NC', + }] + if error_unavailabilityl is not None: + code = error_unavailabilityl.find('code') + label = error_unavailabilityl.find('libelle') + description = error_unavailabilityl.find('descriptif') + severity = error_unavailabilityl.find('severite') + errors += [{ + 'code': code.text if code is not None else 'NC', + 'label': label.text if label is not None else 'NC', + 'description': description.text if description is not None else 'NC', + 'severity': severity.text if severity is not None else 'NC', + }] + if error_protocol is not None: + code = error_protocol.find('code') + label = error_protocol.find('libelle') + description = error_protocol.find('descriptif') + severity = error_protocol.find('severite') + errors += [{ + 'code': code.text if code is not None else 'NC', + 'label': label.text if label is not None else 'NC', + 'description': description.text if description is not None else 'NC', + 'severity': severity.text if severity is not None else 'NC', + }] + + return errors + + # endregion + + def get_base_url(self): + # Give priority to url_root to handle multi-website cases + if request and request.httprequest.url_root: + return request.httprequest.url_root + return super().get_base_url() diff --git a/l10n_fr_payment_payfip/models/payment_transaction.py b/l10n_fr_payment_payfip/models/payment_transaction.py new file mode 100644 index 000000000..28e48b57f --- /dev/null +++ b/l10n_fr_payment_payfip/models/payment_transaction.py @@ -0,0 +1,160 @@ +from datetime import datetime, timedelta +import logging +import uuid +from werkzeug import urls + +from odoo import _, api, fields, models +from odoo.tools import float_round +from odoo.exceptions import ValidationError + +from ..controllers.main import PayFIPController + +_logger = logging.getLogger(__name__) + + +class PayFIPTransaction(models.Model): + # region Private attributes + _inherit = 'payment.transaction' + # endregion + + # region Default methods + # endregion + + # region Fields declaration + payfip_operation_identifier = fields.Char( + string='Operation identifier', + help='Reference of the request of TX as stored in the provider database', + ) + + payfip_return_url = fields.Char( + string='Return URL', + ) + + payfip_sent_to_webservice = fields.Boolean( + string="Sent to PayFIP webservice", + default=False, + ) + + payfip_state = fields.Selection( + string="PayFIP state", + selection=[ + ('P', "Effective payment (P)"), + ('V', "Effective payment (V)"), + ('A', "Abandoned payment (A)"), + ('R', "Other cases (R)"), + ('Z', "Other cases (Z)"), + ('U', "Unknown"), + ] + ) + + payfip_amount = fields.Float( + string="PayFIP amount", + ) + + # endregion + + def create(self, vals): + res = super(PayFIPTransaction, self).create(vals) + if res.provider_id.code == 'payfip': + prec = self.env['decimal.precision'].precision_get('Product Price') + email = res.partner_email + amount = int(float_round(res.amount * 100.0, prec)) + reference = res.reference.replace('-', ' ') + provider_reference = '%.15d' % int( + uuid.uuid4().int % 899999999999999) + res.provider_reference = provider_reference + idop = res.provider_id.payfip_get_id_op_from_web_service( + email, amount, reference, provider_reference) + res.payfip_operation_identifier = idop + return res + + def _get_specific_rendering_values(self, processing_values): + res = super()._get_specific_rendering_values(processing_values) + if self.provider_code != 'payfip': + return res + + base_url = self.provider_id.get_base_url() + if self.provider_id.state == 'enabled': + saisie_value = 'W' + elif self.provider_id.state == 'activation': + saisie_value = 'X' + else: + saisie_value = 'T' + + return { + 'api_url': PayFIPController._payment_url, + 'numcli': self.provider_id.payfip_customer_number, + 'exer': fields.Datetime.now().year, + 'refdet': self.provider_reference, + 'objet': self.reference, + 'montant': self.amount, + 'mel': self.partner_email, + 'urlnotif': self.provider_id.payfip_notification_url, + 'urlredirect': self.provider_id.payfip_redirect_url, + 'saisie': saisie_value, + } + + @api.model + def _get_tx_from_notification_data(self, provider, data): + """ Override of payment to find the transaction based on Payfip data. + + param str provider: The provider of the provider that handled the transaction + :param dict data: The feedback data sent by the provider + :return: The transaction if found + :rtype: recordset of `payment.transaction` + :raise: ValidationError if the data match no transaction + """ + tx = super()._get_tx_from_notification_data(provider, data) + if provider != 'payfip': + return tx + + reference = data + tx = self.sudo().search( + [('payfip_operation_identifier', '=', reference), ('provider_code', '=', 'payfip')]) + if not tx: + raise ValidationError( + "PayFIP: " + + _("No transaction found matching reference %s.", reference) + ) + return tx + + def _process_notification_data(self, feedback_data): + data = self.provider_id.payfip_get_result_from_web_service( + feedback_data) + refdet = data.get('refdet', False) + self.provider_reference = refdet + if data.get('code'): + self._set_pending() + result = data.get('resultrans', False) + self.ensure_one() + if not result: + self._set_pending() + + payfip_amount = int(data.get('montant', 0)) / 100 + if result in ['P', 'V']: + self._set_done() + self.write({ + 'payfip_state': result, + 'payfip_amount': payfip_amount, + }) + return True + elif result in ['A']: + message = 'Received notification for PayFIP payment %s: set as canceled' % self.reference + _logger.info(message) + self._set_canceled() + self.write({ + 'payfip_state': result, + 'payfip_amount': payfip_amount, + }) + return True + elif result in ['R', 'Z']: + message = 'Received notification for PayFIP payment %s: set as error' % self.reference + _logger.info(message) + self._set_error( + state_message=message + ) + self.write({ + 'payfip_state': result, + 'payfip_amount': payfip_amount, + }) + return True diff --git a/l10n_fr_payment_payfip/setup.py b/l10n_fr_payment_payfip/setup.py new file mode 100644 index 000000000..f800476bf --- /dev/null +++ b/l10n_fr_payment_payfip/setup.py @@ -0,0 +1,9 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon={ + 'depends_override': { + } + }, +) diff --git a/l10n_fr_payment_payfip/setup/.setuptools-odoo-make-default-ignore b/l10n_fr_payment_payfip/setup/.setuptools-odoo-make-default-ignore new file mode 100644 index 000000000..207e61533 --- /dev/null +++ b/l10n_fr_payment_payfip/setup/.setuptools-odoo-make-default-ignore @@ -0,0 +1,2 @@ +# addons listed in this file are ignored by +# setuptools-odoo-make-default (one addon per line) diff --git a/l10n_fr_payment_payfip/setup/README b/l10n_fr_payment_payfip/setup/README new file mode 100644 index 000000000..a63d633e8 --- /dev/null +++ b/l10n_fr_payment_payfip/setup/README @@ -0,0 +1,2 @@ +To learn more about this directory, please visit +https://pypi.python.org/pypi/setuptools-odoo diff --git a/l10n_fr_payment_payfip/setup/payment_payfip/odoo/addons/payment_payfip b/l10n_fr_payment_payfip/setup/payment_payfip/odoo/addons/payment_payfip new file mode 100644 index 000000000..fa21be9e2 --- /dev/null +++ b/l10n_fr_payment_payfip/setup/payment_payfip/odoo/addons/payment_payfip @@ -0,0 +1 @@ +../../../../payment_payfip/ \ No newline at end of file diff --git a/l10n_fr_payment_payfip/setup/payment_payfip/setup.cfg b/l10n_fr_payment_payfip/setup/payment_payfip/setup.cfg new file mode 100644 index 000000000..3c6e79cf3 --- /dev/null +++ b/l10n_fr_payment_payfip/setup/payment_payfip/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 diff --git a/l10n_fr_payment_payfip/setup/payment_payfip/setup.py b/l10n_fr_payment_payfip/setup/payment_payfip/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/l10n_fr_payment_payfip/setup/payment_payfip/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/l10n_fr_payment_payfip/static/description/icon.png b/l10n_fr_payment_payfip/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..36678edf4baff7a874dd7c6e14915d60330076a7 GIT binary patch literal 4429 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 T;7?hE00000NkvXXu0mjf0ctnM literal 0 HcmV?d00001 diff --git a/l10n_fr_payment_payfip/static/src/img/payfip_icon.png b/l10n_fr_payment_payfip/static/src/img/payfip_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..36678edf4baff7a874dd7c6e14915d60330076a7 GIT binary patch literal 4429 zcmV-T5wh-yP)1^@s67{VYS00006VoOIv0RI60 z0RN!9r;`8x010qNS#tmYOyvLoOyvP}&hV80000McNliru-~WFU8GbZ8()Nlj2>E@cM*01&ZBL_t(|+U=crcvRKh z$G>-(napG%AtWIQ`w{|#oeWP$1w9*)7J<}ZbD#82=dPuSiNs=H*G9+0h**XYGl08x` z<89p(X*pMMJ}n#PQ?qeCEgM#w4FF)Z2_%~b88>_|cCG)Pt}ob~{Cm$)0Kn$$`yq%D zL`j0#V#966NH+B{*wwGMc0a*dl5wQ~iRszcy!`+KQH05Cg(yiRnMzza`bqZ|tR@xuil3QYsqicZpMl8mr!rA+|z+E=yhJVPsVz) z+&CZ^ugy56`ycan4MCXzC8o1TC0 z`Bwmd>e_k$K#j4kNw|CM)AB^s+-17Xr%xV(6X!2=*#>{qfIf&C(5LD7qWI|mfSrd< z0syw`Is^cys;NU|tq}|uuq@+NJgRDp)W~m(k#m;WRU@5s=G2*de&kzxxa%-X78@$6 zYhkt7T+V0~s|`0#Zl?^^8dE03;G44-+g*;SJkx9TiPQLj)%w~$=UYE9NqB4;5+0j| zyc>o1=!+vbb|x8Rl{Mfv_O2%eJ~}N`k!>+5YJe(&U3`C}*X)yD_!#&{#5Fzl4W9(R zh)E5Na^G-pT0gM7W7opqz7bga(leM46NQg=A4UGnBA6^z@Eq5n+ux-G(`nQ>3*vnY zUb9c;@=TaYn@?VpiyuH~95&Z`!n5!VkA;7DELd#-IITalzM*#O&!0I3^Jh-M?>BA5 z_sQwVyIusIZ(Xv|ifV)g2T~i%8Cm%^FngtY*VCM}!G8Fw?|reqvS5ZHE086?(0ddN zp|OaVxDvX6zR>xHIV^weRxy^nwH`UwZb8$EWXltNf>lcsRNKZbHDo$^P+xQ>SdAW> z&H!F-fT=tSMJXTS?50>`A6bp6f*)Y1EwWpFKxA)x`1VVP9@qzx8W>Bj;MA5U5u309kui_h zHRv<*3Q$#R^jLgIu=WN}-xIt~kZR=#rZNboGAG_??4)sDvOFAo=TR(Le--{A0~#Cs zVR-MY=aG4(fCTea0te1SaI|`-_4TEf?E1ZKV=7B;eq5emCtgeW+LFx2PlIziS7GYX zEzlWy001IGdm%Ek7YXLA7}VZ(f2VF#w}? ze!#T`Tsph~eMUZv!0@3Y7;!i89!1cgM`1M);4kcZJ zsPTg=diF;vl)<_oCDk=JeK{8l6igXDlmsKx<=_t~Fj*SrfC}poK!OqKQCeM#GnaFl z?yDc!Gl&EuRH_=QYVnudU%_N;$X=2pBc@*@HS&q}Q(Re%KYxA@*UKxQ(>B~*AhK5= z^d7jgGzr$tD5|K!J3IE{MtKEvErgmqag4`5n*{6P6joH??X6$pR%I1*E#@M!2_j;L z4kW>dd*{79N0E|$4Mi0-(CX}O@icw(aMf=6PJ(qaZj_c|&$s7sJUIYzqn2g(xU4!>;3}k(_-EwPq6}SplbUJj8u^^+bR{Pl6GyajmcfJC1#aq^x`x z&1MLa42^~Z&pRGov(1Lr6XtZ-wvk}>DfzdGk(yh8?MJ?aRTNQEXM!L}&}ew@JnwWG zKS`GH^1N9H4)7zv2uF5*eHvgGY&&!UHbI0<6k)8lfaMw{A>}x(&G{kwhlSwBBceKV zn@KP)<^PVH0vI4AD<3I2SHUn0>dfYb{}wA)h5^epO69`DqZXThHNTigv#ogpKAh{n z6*OpFpHX)ES0@}_Y+QZI7e|_3Vi}msRxm6BK@?$=WH1Z^o@2o7iF$cejay~*3hLqTnowWddcbZU)W(0j#viQC zulXg$$+C@)>rIxX_ZcvDkLxWKJHa@%IWw`hg=&1xuxyLxZ1Zv~3y$Z&aU3|lA-f5( zj5SaH9C0Itg6CKg>`pSS6d*A@8=JQufFOzxM8{deOy=f;EsLujXyphsoY1DB*=<_jlW%n@zH}i|H<3>ee(x_o}t3S7JPM7^cc~mf2ma$>$mu`!3yBcVG()2iP zbTs0|M7LSanstA3Ya7?Be%ax*)PsL?O(_Nli<*Y8sHun>9gT@GQ6TP!m8Y&nDvE-I zzg>%@^qe*YyZ!OEGaSn@b|S@%a__)tdi8T_JB9M=4zF$hwdjvB zvaI0r#SA2-=d?PcCmEoCX zNiY@o{z5t~WaQd)0Ny_6dLJEr^VAbq_On?Y+aZ=~|1P=HcS+rO9VnP2N!Yk;uY+71 z$HGUe#b@uX#;7RI{#?D^_y^lv#^AxOFWa*xNz`G2eVdeq8=A}W9A-=&hxgukfg;^aQSC~pAc{!I%CqaL zcrbPh$wh*x!#m_X4aJR_LZCScwSsvZds2{v`&D2gQEIl(@AcQr+l@N655AV@HR1S3c= zf*`>N5{w|h2!aG7NHBr~BM1_VAi)TN1S3c=f&?Q75{w|hybW12QxHfn&k;1l^*dcN zoDT(z1apsoz7M#(Y-R2~3K$9I9*Sghd0FGl(-bfg%!2{sbrgi|B3MCTi8_sZfg1TF zm=jJVrKwU*@bCmOg9LM=+E}kjIffAWAu$Q&g#7D8s+A=PCbEMByR(W+RjsVhn5W1N z63o7VUs+qHT6y~5VPpph*36b&hrzMx?T$~7>w=YE-bp5PWx;L~m*K#PbE=lvW8e&z zm$g)8lSy4wup33CSh;2+jP>d^%6VN6mmB&Ft09xRqF@J4p2PCrZ$Mh^Rl6w!Rcq*v za;>4y@^rzw)i0l?CWZH8WByWhKDO=q8aGSIQCM2xbaq`=xpMTAR^PML72(3>{$RPb zGnCdp662QVkWJ2Ng8N}3_kME*0I+rMe_%A3VXQZyu(T2!%eG6dyP@?9hhNxOn94KS zT+UikKqlQ!f?d5?jFhZAq+HHJN>(0<%PRo@#bs4s7zPZ(K&$DPP519J<*tG;EKerg z4}!h9;S;1>&O@Eqf_k$V^(G7I&6cM2l}6L?n;ZFGucIhwJy>4f=5qC=sqhUQO-6aM zV0-@aJ?hO?FbtY9l_#q}SI`(j5H|K#C`;ek=5jR!-;iM5eimu93DEEyc#Z?dvS1m; zE4HiOw6%~$OWO-664~VK8O$b#-WepHAoL!;5+x~HT5WF?C9X%rq*ur&Zx-z7CuVwW zp!!bz6Y7eRU@CJy=1eeGpsqL>e!a(&RV`0|ys00o&eZWEN&O6ZmzS-~*@vt>KLw{T zIIl{l{t4l+FOpT>Dwww;OEy&I?nmyvg-*xf2M&A$!{&WSf%4WyzSjqf(;|52T!hEI z2t^j1oJpRbAdz70O!=G!eINJ(e0xuTB1;Y#t~hx!1&ON2_9Izqae3D)7;l}m>x5-E zA540&f&xSttbK6001Qt!iedBj+x3yjq8UX=8!14P!PG&KB~)KMhOAxjU^p!pmIK3T zG3li$3Jzs36~3V?hrof4VA9J*_(#S;mTZtEE3)@ILBXL6rgk@m6}8t-;qvFR!16kb zc=9;>BjQLfQbWZmQ<0%aLgNX*IZS@VM1qkPo@kqiP!#2^qhU!ff}ln|;jR2XCl=a@ T;7?hE00000NkvXXu0mjf0ctnM literal 0 HcmV?d00001 diff --git a/l10n_fr_payment_payfip/tests/__init__.py b/l10n_fr_payment_payfip/tests/__init__.py new file mode 100644 index 000000000..7f6f7f535 --- /dev/null +++ b/l10n_fr_payment_payfip/tests/__init__.py @@ -0,0 +1,4 @@ + +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import common +from . import test_payfip \ No newline at end of file diff --git a/l10n_fr_payment_payfip/tests/common.py b/l10n_fr_payment_payfip/tests/common.py new file mode 100644 index 000000000..7bc8f4c1f --- /dev/null +++ b/l10n_fr_payment_payfip/tests/common.py @@ -0,0 +1,18 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo.addons.payment.tests.common import PaymentCommon + + +class PayFIPCommon(PaymentCommon): + + @classmethod + def setUpClass(cls, chart_template_ref='l10n_fr.l10n_fr_pcg_chart_template'): + super().setUpClass(chart_template_ref=chart_template_ref) + + cls.payfip = cls._prepare_provider('payfip', update_values={ + 'payfip_customer_number': '006382', + 'payfip_form_action_url': "https://www.tipi.budget.gouv.fr/tpa/paiementws.web", + }) + + # Override default values + cls.provider = cls.payfip + cls.currency = cls.currency_euro diff --git a/l10n_fr_payment_payfip/tests/test_payfip.py b/l10n_fr_payment_payfip/tests/test_payfip.py new file mode 100644 index 000000000..f7cfab903 --- /dev/null +++ b/l10n_fr_payment_payfip/tests/test_payfip.py @@ -0,0 +1,155 @@ + +from odoo.exceptions import ValidationError +from odoo.tools import mute_logger +from odoo.tests import tagged, users +from odoo import _, fields +from odoo.tests import HttpCase +import pytest + +from .common import PayFIPCommon +from ..controllers.main import PayFIPController + + +@pytest.mark.skip( + reason=( + "No way of currently testing this with pytest-odoo because:\n" + "* that require an open odoo port\n" + "* the open port should use the same pgsql transaction\n" + "* payment.transaction class must be mocked\n\n" + "Please use odoo --test-enable to launch those test" + ) +) +@tagged('post_install', '-at_install') +class PayFIPHttpTest(PayFIPCommon, HttpCase): + @classmethod + def setUpClass(cls): + @classmethod + def base_url(cls): + return cls.env["ir.config_parameter"].get_param("web.base.url") + cls.base_url = base_url + super().setUpClass() + + def setUp(self): + super().setUp() + self.PaymentTransaction = self.env["payment.transaction"] + self.calls = 0 + + def handle_feedback_data(_, provider, data): + self.assertEqual(provider, "payfip") + self.assertEqual( + data, {"idop": "5e64f6f2-7b4b-4ebe-aa6c-7493f5e443af"}) + self.calls += 1 + return self.PaymentTransaction.new({"reference": "5e64f6f2-7b4b-4ebe-aa"}) + + self.PaymentTransaction._patch_method( + "_handle_feedback_data", handle_feedback_data) + self.addCleanup(self.PaymentTransaction._revert_method, + "_handle_feedback_data") + + def test_return_get(self): + idop = "5e64f6f2-7b4b-4ebe-aa6c-7493f5e443af" + url = self._build_url(PayFIPController._return_url) + response = self.opener.get( + url + "?idop=" + idop) + self.assertEqual(response.status_code, 200) + self.assertEqual(self.calls, 1) + response = self.opener.post( + url, data={"idop": idop}) + self.assertEqual(response.status_code, 200) + self.assertEqual(self.calls, 2) + + def test_notify_post(self): + idop = "5e64f6f2-7b4b-4ebe-aa6c-7493f5e443af" + url = self._build_url(PayFIPController._notification_url) + response = self.opener.post( + url, data={"idop": idop}) + self.assertEqual(response.status_code, 200) + self.assertEqual(self.calls, 1) + + +@tagged('post_install', '-at_install') +class PayFIPTest(PayFIPCommon): + + def test_processing_values(self): + tx = self.create_transaction(flow='redirect') # Only flow implemented + return_url = self._build_url(PayFIPController._return_url) + notification_url = self._build_url(PayFIPController._notification_url) + + expected_values = { + 'numcli': self.provider.payfip_customer_number, + 'exer': str(fields.Datetime.now().year), + 'mel': 'norbert.buyer@example.com', + 'montant': "1111.11", + 'objet': self.reference, + 'refdet': tx.provider_reference, + 'saisie': 'T', + 'urlnotif': notification_url, + 'urlredirect': return_url + } + + with mute_logger('odoo.addons.payment.models.payment_transaction'): + processing_values = tx._get_processing_values() + redirect_form_data = self._extract_values_from_html_form( + processing_values['redirect_form_html']) + + self.assertEqual( + redirect_form_data['action'], self.provider.payfip_get_form_action_url()) + + self.assertDictEqual( + expected_values, + redirect_form_data['inputs'], + "PayFIP: invalid inputs specified in the redirect form.", + ) + + def test_feedback_processing(self): + # typical data posted by payFIP after client has successfully paid + payfip_post_data = { + 'dattrans': u'06012023', + 'exer': u'2023', + 'heurtrans': u'1100', + 'idOp': u'93be8501-9184-4e63-81b3-53b5a7f4d69a', + 'mel': u'norbert.buyer@example.com', + 'montant': u'111111', + 'numauto': u'A55A', + 'numcli': self.provider.payfip_customer_number, + 'objet': self.reference, + 'refdet': u'088655675121650', + 'saisie': u'T' + } + + with self.assertRaises(ValidationError): + self.env['payment.transaction']._handle_feedback_data( + 'payfip', payfip_post_data['idOp']) + + tx = self.create_transaction(flow='redirect') + + self.env['payment.transaction']._handle_feedback_data( + 'payfip', payfip_post_data) + self.assertEqual( + tx.state, 'pending', 'payfip: wrong state after receiving a valid pending notification') + self.assertEqual(tx.provider_reference, '088655675121650', + 'payfip: wrong reference after receiving a valid pending notification') + + tx.write({'state': 'draft', 'provider_reference': False}) + + payfip_post_data = 'd4a40f3e-5186-4e3f-a74c-213279cb82f1' + self.env['payment.transaction']._handle_feedback_data( + 'payfip', payfip_post_data) + self.assertEqual( + tx.state, 'done', 'payfip: wrong state after receiving a valid done notification') + self.assertEqual(tx.provider_reference, '088655675121650', + 'payfip: wrong reference after receiving a valid done notification') + + tx.write({'state': 'draft', 'provider_reference': False}) + + payfip_post_data = '580f47c5-dc72-40d3-86c0-ae104ef48797' + self.env['payment.transaction']._handle_feedback_data( + 'payfip', payfip_post_data) + self.assertEqual( + tx.state, 'cancel', 'wrong state after receiving a valid cancel notification') + self.assertEqual(tx.provider_reference, '088655675121650', + 'payfip: wrong reference after receiving a valid cancel notification') + + def test_payfip_webservice(self): + payfip_webservice_enable = self.provider._check_payfip_customer_number() + self.assertEqual(payfip_webservice_enable, True) diff --git a/l10n_fr_payment_payfip/views/payment_payfip_templates.xml b/l10n_fr_payment_payfip/views/payment_payfip_templates.xml new file mode 100644 index 000000000..281d8f810 --- /dev/null +++ b/l10n_fr_payment_payfip/views/payment_payfip_templates.xml @@ -0,0 +1,16 @@ + + + + diff --git a/l10n_fr_payment_payfip/views/payment_views.xml b/l10n_fr_payment_payfip/views/payment_views.xml new file mode 100644 index 000000000..0ca6f9854 --- /dev/null +++ b/l10n_fr_payment_payfip/views/payment_views.xml @@ -0,0 +1,56 @@ + + + + provider.form.payfip + payment.provider + + + + + + + + + + + + + + + Add operation identifier from PayFIP provider + payment.transaction + + + + + + + + + + + + + + + + + + + Add decorators for PayFIP transactions + payment.transaction + + + + provider_code == 'payfip' and payfip_state == False + provider_code == 'payfip' and payfip_state != False and payfip_amount != amount + + + + + + + + + + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c6d72a46f..f1b847f8e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ # generated from manifests external_dependencies +openupgradelib +pgpy pyfrdas2 pypdf>=3.1.0 python-stdnum diff --git a/setup/l10n_fr_payment_payfip/odoo/addons/l10n_fr_payment_payfip b/setup/l10n_fr_payment_payfip/odoo/addons/l10n_fr_payment_payfip new file mode 120000 index 000000000..8b92e783c --- /dev/null +++ b/setup/l10n_fr_payment_payfip/odoo/addons/l10n_fr_payment_payfip @@ -0,0 +1 @@ +../../../../l10n_fr_payment_payfip \ No newline at end of file diff --git a/setup/l10n_fr_payment_payfip/setup.py b/setup/l10n_fr_payment_payfip/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/l10n_fr_payment_payfip/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 387441a7a5b218cf35f8870d357eff6500586ecc Mon Sep 17 00:00:00 2001 From: Romain <73525560+Rom10811@users.noreply.github.com> Date: Wed, 10 Jul 2024 15:13:49 +0200 Subject: [PATCH 2/2] Update main.py --- l10n_fr_payment_payfip/controllers/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/l10n_fr_payment_payfip/controllers/main.py b/l10n_fr_payment_payfip/controllers/main.py index 06b999c71..637c8ae8e 100644 --- a/l10n_fr_payment_payfip/controllers/main.py +++ b/l10n_fr_payment_payfip/controllers/main.py @@ -35,7 +35,7 @@ def payfip_pay(self, **post): 'payfip_sent_to_webservice': True, }) return werkzeug.utils.redirect('{url}?idop={idop}'.format( - url="https://www.tipi.budget.gouv.fr/tpa/paiementws.web", + url="https://www.payfip.gouv.fr/tpa/paiementws.web", idop=tx.payfip_operation_identifier, )) else: