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.
+
+
+
+
+
+
+
+ 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
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
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: