diff --git a/account_invoice_qr_code_sepa_payconiq/README.rst b/account_invoice_qr_code_sepa_payconiq/README.rst new file mode 100644 index 00000000000..accf01e043b --- /dev/null +++ b/account_invoice_qr_code_sepa_payconiq/README.rst @@ -0,0 +1,95 @@ +===================================== +Account Invoice Qr Code Sepa Payconiq +===================================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--invoicing-lightgray.png?logo=github + :target: https://github.com/OCA/account-invoicing/tree/14.0/account_invoice_qr_code_sepa_payconiq + :alt: OCA/account-invoicing +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/account-invoicing-14-0/account-invoicing-14-0-account_invoice_qr_code_sepa_payconiq + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/95/14.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to generate a QR code to be displayed on invoices. + +This differs from the SEPA QR Code as Payconiq in some countries (e.g.: LU) +requires invoices emitters to pay a fee per paid transactions through this mean. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +#. Go to Accounting > Settings > Customer Payments > QR Codes + +Fill in the Payconiq Profile Id you've been given. + +Usage +===== + +See Odoo documentation for SEPA QR code. + +https://www.odoo.com/documentation/master/applications/finance/accounting/receivables/customer_invoices/epc_qr_code.html + +Known issues / Roadmap +====================== + +* At the time being, this is only available in Luxembourg. + +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 smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ACSONE SA/NV + +Contributors +~~~~~~~~~~~~ + +* Denis Roussel + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/account-invoicing `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_invoice_qr_code_sepa_payconiq/__init__.py b/account_invoice_qr_code_sepa_payconiq/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/account_invoice_qr_code_sepa_payconiq/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/account_invoice_qr_code_sepa_payconiq/__manifest__.py b/account_invoice_qr_code_sepa_payconiq/__manifest__.py new file mode 100644 index 00000000000..5ae595f6bc0 --- /dev/null +++ b/account_invoice_qr_code_sepa_payconiq/__manifest__.py @@ -0,0 +1,18 @@ +# Copyright 2022 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Account Invoice Qr Code Sepa Payconiq", + "summary": """ + Allows to generate a qr code for Payconiq provider containing the url""", + "version": "14.0.1.0.0", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/account-invoicing", + "depends": [ + "account_qr_code_sepa", + ], + "data": [ + "views/res_config_settings.xml", + ], +} diff --git a/account_invoice_qr_code_sepa_payconiq/models/__init__.py b/account_invoice_qr_code_sepa_payconiq/models/__init__.py new file mode 100644 index 00000000000..62d0ee6676f --- /dev/null +++ b/account_invoice_qr_code_sepa_payconiq/models/__init__.py @@ -0,0 +1 @@ +from . import res_company, res_config_settings, res_partner_bank diff --git a/account_invoice_qr_code_sepa_payconiq/models/res_company.py b/account_invoice_qr_code_sepa_payconiq/models/res_company.py new file mode 100644 index 00000000000..07eb358fa24 --- /dev/null +++ b/account_invoice_qr_code_sepa_payconiq/models/res_company.py @@ -0,0 +1,13 @@ +# Copyright 2022 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResCompany(models.Model): + + _inherit = "res.company" + + payconiq_qr_profile_id = fields.Char( + string="Payconiq Profile Id", help="Fill in this with your payment profile Id" + ) diff --git a/account_invoice_qr_code_sepa_payconiq/models/res_config_settings.py b/account_invoice_qr_code_sepa_payconiq/models/res_config_settings.py new file mode 100644 index 00000000000..84513b6f375 --- /dev/null +++ b/account_invoice_qr_code_sepa_payconiq/models/res_config_settings.py @@ -0,0 +1,12 @@ +# Copyright 2022 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + payconiq_qr_profile_id = fields.Char( + related="company_id.payconiq_qr_profile_id", + readonly=False, + ) diff --git a/account_invoice_qr_code_sepa_payconiq/models/res_partner_bank.py b/account_invoice_qr_code_sepa_payconiq/models/res_partner_bank.py new file mode 100644 index 00000000000..3b1950babbd --- /dev/null +++ b/account_invoice_qr_code_sepa_payconiq/models/res_partner_bank.py @@ -0,0 +1,177 @@ +# Copyright 2022 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import base64 +import io +import urllib + +import requests +from PIL import Image + +from odoo import _, api, models +from odoo.tools.image import image_data_uri + +PAYCONIQ_URL = "https://payconiq.com/t/1/" +PAYCONIQ_QR_URL = "https://portal.payconiq.com/qrcode" + + +class ResPartnerBank(models.Model): + + _inherit = "res.partner.bank" + + @api.model + def _get_available_qr_methods(self): + rslt = super()._get_available_qr_methods() + rslt.append(("payconiq_qr", _("Payconiq QR"), 19)) + return rslt + + def _get_payconiq_qr_amount(self, amount): + """ + Payconiq is awaiting an amount in eurocents. So, + if the amount is for instance 141.9, reformat to float with + two decimals => 141.90, then remove decimal dot. + """ + return f"{amount*100:.0f}" + + def _get_qr_code_generation_params( + self, + qr_method, + amount, + currency, + debtor_partner, + free_communication, + structured_communication, + ): + """ + https://developer.payconiq.com/online-payments-dock/#creating-the-payconiq-qr-code77 + + """ + if qr_method == "payconiq_qr": + return { + "payconiq_qr": True, + "D": "Payment", + "A": self._get_payconiq_qr_amount(amount), + "R": structured_communication + if structured_communication + else free_communication, + } + return super()._get_qr_code_generation_params( + qr_method, + amount, + currency, + debtor_partner, + free_communication, + structured_communication, + ) + + def _get_qr_code_url( + self, + qr_method, + amount, + currency, + debtor_partner, + free_communication, + structured_communication, + ): + params = self._get_qr_code_generation_params( + qr_method, + amount, + currency, + debtor_partner, + free_communication, + structured_communication, + ) + if params and params.get("payconiq", False): + params.pops("payconiq_qr") + return urllib.parse.urlencode(params) + return super()._get_qr_code_url( + qr_method, + amount, + currency, + debtor_partner, + free_communication, + structured_communication, + ) + + def _eligible_for_qr_code(self, qr_method, debtor_partner, currency): + if qr_method == "payconiq_qr": + return ( + currency.name == "EUR" + and self.acc_type == "iban" + and self.sanitized_acc_number[:2] in ["LU"] + ) + return super()._eligible_for_qr_code(qr_method, debtor_partner, currency) + + def _get_qr_code_base64( + self, + qr_method, + amount, + currency, + debtor_partner, + free_communication, + structured_communication, + ): + """ + The url to get QR code image from Payconiq is composed by several parameters: + - The type of the image + - The image size + - The redirection url (to launch the payconiq application - this is the url + inside the QR code itself) + """ + params = self._get_qr_code_generation_params( + qr_method, + amount, + currency, + debtor_partner, + free_communication, + structured_communication, + ) + if params and params.pop("payconiq_qr", False): + profile_id = self.env.company.payconiq_qr_profile_id + # Build url that would be contained in QR code + c_url = PAYCONIQ_URL + profile_id + "?" + new_params = { + "f": "PNG", + "s": "S", + "c": c_url + urllib.parse.urlencode(params), + } + response = requests.get(PAYCONIQ_QR_URL, params=new_params, stream=True) + raw_image = response.raw + img = Image.open(raw_image) + + buffer = io.BytesIO() + img.save(buffer, format="PNG") + myimage = buffer.getvalue() + barcode = image_data_uri(base64.b64encode(myimage)) + return barcode + return super()._get_qr_code_base64( + qr_method, + amount, + currency, + debtor_partner, + free_communication, + structured_communication, + ) + + def _check_for_qr_code_errors( + self, + qr_method, + amount, + currency, + debtor_partner, + free_communication, + structured_communication, + ): + if qr_method == "payconiq_qr": + if not self.env.company.payconiq_qr_profile_id: + return _( + "You should provide a Payconiq Profile Id (Accounting > Settings > " + "Customer Payments > QR Codes" + ) + return super()._check_for_qr_code_errors( + qr_method, + amount, + currency, + debtor_partner, + free_communication, + structured_communication, + ) diff --git a/account_invoice_qr_code_sepa_payconiq/readme/CONFIGURE.rst b/account_invoice_qr_code_sepa_payconiq/readme/CONFIGURE.rst new file mode 100644 index 00000000000..f1f286b1c86 --- /dev/null +++ b/account_invoice_qr_code_sepa_payconiq/readme/CONFIGURE.rst @@ -0,0 +1,3 @@ +#. Go to Accounting > Settings > Customer Payments > QR Codes + +Fill in the Payconiq Profile Id you've been given. diff --git a/account_invoice_qr_code_sepa_payconiq/readme/CONTRIBUTORS.rst b/account_invoice_qr_code_sepa_payconiq/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..9179ee4b8fa --- /dev/null +++ b/account_invoice_qr_code_sepa_payconiq/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Denis Roussel diff --git a/account_invoice_qr_code_sepa_payconiq/readme/DESCRIPTION.rst b/account_invoice_qr_code_sepa_payconiq/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..8b77cb5dc94 --- /dev/null +++ b/account_invoice_qr_code_sepa_payconiq/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +This module allows to generate a QR code to be displayed on invoices. + +This differs from the SEPA QR Code as Payconiq in some countries (e.g.: LU) +requires invoices emitters to pay a fee per paid transactions through this mean. diff --git a/account_invoice_qr_code_sepa_payconiq/readme/ROADMAP.rst b/account_invoice_qr_code_sepa_payconiq/readme/ROADMAP.rst new file mode 100644 index 00000000000..4e5c97ed73e --- /dev/null +++ b/account_invoice_qr_code_sepa_payconiq/readme/ROADMAP.rst @@ -0,0 +1 @@ +* At the time being, this is only available in Luxembourg. diff --git a/account_invoice_qr_code_sepa_payconiq/readme/USAGE.rst b/account_invoice_qr_code_sepa_payconiq/readme/USAGE.rst new file mode 100644 index 00000000000..5dd42855938 --- /dev/null +++ b/account_invoice_qr_code_sepa_payconiq/readme/USAGE.rst @@ -0,0 +1,3 @@ +See Odoo documentation for SEPA QR code. + +https://www.odoo.com/documentation/master/applications/finance/accounting/receivables/customer_invoices/epc_qr_code.html diff --git a/account_invoice_qr_code_sepa_payconiq/static/description/icon.png b/account_invoice_qr_code_sepa_payconiq/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/account_invoice_qr_code_sepa_payconiq/static/description/icon.png differ diff --git a/account_invoice_qr_code_sepa_payconiq/static/description/index.html b/account_invoice_qr_code_sepa_payconiq/static/description/index.html new file mode 100644 index 00000000000..167588d86b7 --- /dev/null +++ b/account_invoice_qr_code_sepa_payconiq/static/description/index.html @@ -0,0 +1,442 @@ + + + + + + +Account Invoice Qr Code Sepa Payconiq + + + +
+

Account Invoice Qr Code Sepa Payconiq

+ + +

Beta License: AGPL-3 OCA/account-invoicing Translate me on Weblate Try me on Runbot

+

This module allows to generate a QR code to be displayed on invoices.

+

This differs from the SEPA QR Code as Payconiq in some countries (e.g.: LU) +requires invoices emitters to pay a fee per paid transactions through this mean.

+

Table of contents

+ +
+

Configuration

+
    +
  1. Go to Accounting > Settings > Customer Payments > QR Codes
  2. +
+

Fill in the Payconiq Profile Id you’ve been given.

+
+ +
+

Known issues / Roadmap

+
    +
  • At the time being, this is only available in Luxembourg.
  • +
+
+
+

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 smashing it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE SA/NV
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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

+

This module is part of the OCA/account-invoicing project on GitHub.

+

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

+
+
+
+ + diff --git a/account_invoice_qr_code_sepa_payconiq/tests/__init__.py b/account_invoice_qr_code_sepa_payconiq/tests/__init__.py new file mode 100644 index 00000000000..9022bb55eba --- /dev/null +++ b/account_invoice_qr_code_sepa_payconiq/tests/__init__.py @@ -0,0 +1 @@ +from . import test_payconiq_qr diff --git a/account_invoice_qr_code_sepa_payconiq/tests/test_payconiq_qr.py b/account_invoice_qr_code_sepa_payconiq/tests/test_payconiq_qr.py new file mode 100644 index 00000000000..afcb5868496 --- /dev/null +++ b/account_invoice_qr_code_sepa_payconiq/tests/test_payconiq_qr.py @@ -0,0 +1,120 @@ +# Copyright 2022 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import urllib + +import mock +import qrcode +import requests +import urllib3 +from urllib3._collections import HTTPHeaderDict + +from odoo.tests import Form, SavepointCase + + +def get_image(): + params = { + "D": "Payment", + "A": "3090", + "R": "test", + } + return qrcode.make( + "https://payconiq.com/t/1/1234567890?" + urllib.parse.urlencode(params) + ) + + +def mocked_requests_get(*args, **kwargs): + headers = HTTPHeaderDict({"Content-Type": "image/png"}) + http_response = urllib3.response.HTTPResponse( + request_method="GET", headers=headers, status=200 + ) + + response = requests.Response() + response.status_code = 200 + response.raw = http_response + return response + + +class TestAccountInvoicePayconiq(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + # Create a Luxembourgish company with at least a bank account + cls.company = cls.env["res.company"].create( + { + "name": "Lux Company", + } + ) + cls.env["account.chart.template"].browse(1).with_company( + cls.company + ).try_loading() + cls.company.currency_id = cls.env.ref("base.EUR") + pricelist = cls.env["product.pricelist"].create( + { + "name": "Pricelist EUR", + "company_id": cls.company.id, + "currency_id": cls.env.ref("base.EUR").id, + "item_ids": [ + ( + 0, + 0, + { + "applied_on": "3_global", + "compute_price": "formula", + "base": "list_price", + }, + ) + ], + } + ) + cls.env["res.partner.bank"].create( + { + "acc_number": "LU 28 001 9400644750000", + "partner_id": cls.company.partner_id.id, + "company_id": cls.company.id, + } + ) + # Set the Payconiq profile + cls.company.payconiq_qr_profile_id = "1234567890" + cls.account_move = cls.env["account.move"] + # Create a customer invoice + invoice_form = Form( + cls.account_move.with_context(default_move_type="out_invoice").with_company( + cls.company + ) + ) + cls.user = cls.env["res.users"].create( + { + "name": "My Lux User", + "login": "lux_user", + "company_id": cls.company.id, + "company_ids": [(4, cls.company.id)], + } + ) + cls.user.groups_id |= cls.env.ref("account.group_account_manager") + # Change Environment to make all operations in user's Lux company + cls.env = cls.env( + context=dict(cls.env.context, tracking_disable=True, user=cls.user) + ) + cls.partner = cls.env["res.partner"].create( + {"name": "test partner", "property_product_pricelist": pricelist.id} + ) + invoice_form.partner_id = cls.partner + invoice_form.currency_id = cls.env.ref("base.EUR") + with invoice_form.invoice_line_ids.new() as line_form: + line_form.name = "Test invoice line" + line_form.price_unit = 30.1 + line_form.tax_ids.clear() + cls.invoice = invoice_form.save() + + def test_payconiq(self): + with mock.patch("requests.get", side_effect=mocked_requests_get), mock.patch( + "PIL.Image.open" + ) as image_mock: + image_mock.return_value = get_image() + url = self.invoice.generate_qr_code() + + self.assertTrue(url) + self.assertIn( + "data:image/png;base64", + url, + ) diff --git a/account_invoice_qr_code_sepa_payconiq/views/res_config_settings.xml b/account_invoice_qr_code_sepa_payconiq/views/res_config_settings.xml new file mode 100644 index 00000000000..ab92a1a7b7e --- /dev/null +++ b/account_invoice_qr_code_sepa_payconiq/views/res_config_settings.xml @@ -0,0 +1,25 @@ + + + + + + res.config.settings + +
+
+
+
+
+
+
+
+
diff --git a/setup/account_invoice_qr_code_sepa_payconiq/odoo/addons/account_invoice_qr_code_sepa_payconiq b/setup/account_invoice_qr_code_sepa_payconiq/odoo/addons/account_invoice_qr_code_sepa_payconiq new file mode 120000 index 00000000000..fa3c961f4d6 --- /dev/null +++ b/setup/account_invoice_qr_code_sepa_payconiq/odoo/addons/account_invoice_qr_code_sepa_payconiq @@ -0,0 +1 @@ +../../../../account_invoice_qr_code_sepa_payconiq \ No newline at end of file diff --git a/setup/account_invoice_qr_code_sepa_payconiq/setup.py b/setup/account_invoice_qr_code_sepa_payconiq/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/account_invoice_qr_code_sepa_payconiq/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 00000000000..15aaeb33ec5 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1 @@ +qrcode