diff --git a/README.md b/README.md new file mode 100644 index 0000000..296b108 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# Mollie addon for Odoo 10© +This is the official Mollie addon for Odoo 10© + +## Installation +For installation instructions please refer to the odoo docs: +http://odoo-development.readthedocs.io/en/latest/odoo/usage/install-module.html#from-zip-archive-install + +## Configuration +Go to Invoicing section -> Payments -> Payment Acquirers -> Mollie. +Add API Keys (test and/or live) from your Mollie Account. + +![alt text](/images/odoo_configuration.png "Odoo mollie configuration example") + +When Mollie acquirer is configured correctly, you can see Mollie payment option at the time of checkout. + +Shopper will then be redirected to the Mollie payment method selection screen. + +After a succesfull payment, a confirmation is shown to the shopper: + +![alt text](/images/payment_confirmation.png "Odoo mollie payment confirmation") diff --git a/__init__.py b/__init__.py new file mode 100755 index 0000000..396c76f --- /dev/null +++ b/__init__.py @@ -0,0 +1,2 @@ +import models +import controllers diff --git a/__manifest__.py b/__manifest__.py new file mode 100755 index 0000000..91445a9 --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,18 @@ +# -*- encoding: utf-8 -*- +{ + 'name': 'Mollie acquirer for online payments', + 'version': '1.10', + 'author': 'BeOpen NV', + 'website': 'http://www.beopen.be', + 'category': 'eCommerce', + 'description': "", + 'depends': ['payment','website_sale'], + 'data': [ + 'views/mollie.xml', + 'views/payment_acquirer.xml', + 'data/mollie.xml', + ], + 'installable': True, + 'currency': 'EUR', + 'images': ['images/main_screenshot.png'] +} diff --git a/controllers/__init__.py b/controllers/__init__.py new file mode 100755 index 0000000..bbd183e --- /dev/null +++ b/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +import main diff --git a/controllers/main.py b/controllers/main.py new file mode 100755 index 0000000..85a4b39 --- /dev/null +++ b/controllers/main.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- + +import json +import logging +import requests +import werkzeug +import pprint + + +from odoo import http, SUPERUSER_ID +from odoo.http import request +from odoo.addons.payment.models.payment_acquirer import ValidationError + +_logger = logging.getLogger(__name__) + + +class MollieController(http.Controller): + _notify_url = '/payment/mollie/notify/' + _redirect_url = '/payment/mollie/redirect/' + _cancel_url = '/payment/mollie/cancel/' + + @http.route([ + '/payment/mollie/notify'], + type='http', auth='none', methods=['GET']) + def mollie_notify(self, **post): + cr, uid, context = request.cr, SUPERUSER_ID, request.context + request.env['payment.transaction'].sudo().form_feedback(post, 'mollie') + return werkzeug.utils.redirect("/shop/payment/validate") + + @http.route([ + '/payment/mollie/redirect'], type='http', auth="none", methods=['GET']) + def mollie_redirect(self, **post): + cr, uid, context = request.cr, SUPERUSER_ID, request.context + request.env['payment.transaction'].sudo().form_feedback(post, 'mollie') + return werkzeug.utils.redirect("/shop/payment/validate") + + @http.route([ + '/payment/mollie/cancel'], type='http', auth="none", methods=['GET']) + def mollie_cancel(self, **post): + cr, uid, context = request.cr, SUPERUSER_ID, request.context + request.env['payment.transaction'].sudo().form_feedback(post, 'mollie') + return werkzeug.utils.redirect("/shop/payment/validate") + + @http.route([ + '/payment/mollie/intermediate'], type='http', auth="none", methods=['POST'], csrf=False) + def mollie_intermediate(self, **post): + acquirer = request.env['payment.acquirer'].browse(int(post['Key'])) + + url = post['URL'] + "payments" + headers = {'content-type': 'application/json', 'Authorization': 'Bearer ' + acquirer._get_mollie_api_keys(acquirer.environment)['mollie_api_key'] } + base_url = post['BaseUrl'] + orderid = post['OrderId'] + description = post['Description'] + currency = post['Currency'] + amount = post['Amount'] + language = post['Language'] + name = post['Name'] + email = post['Email'] + zip = post['Zip'] + address = post['Address'] + town = post['Town'] + country = post['Country'] + phone = post['Phone'] + + payload = { + "description": description, + "amount": amount, + #"webhookUrl": base_url + self._notify_url, + "redirectUrl": "%s%s?reference=%s" % (base_url, self._redirect_url, orderid), + "metadata": { + "order_id": orderid, + "customer": { + "locale": language, + "currency": currency, + "last_name": name, + "address1": address, + "zip_code": zip, + "city": town, + "country": country, + "phone": phone, + "email": email + } + } + } + + mollie_response = requests.post( + url, data=json.dumps(payload), headers=headers).json() + + if mollie_response["status"] == "open": + + payment_tx = request.env['payment.transaction'].sudo().search([('reference', '=', orderid)]) + if not payment_tx or len(payment_tx) > 1: + error_msg = ('received data for reference %s') % (pprint.pformat(orderid)) + if not payment_tx: + error_msg += ('; no order found') + else: + error_msg += ('; multiple order found') + _logger.info(error_msg) + raise ValidationError(error_msg) + payment_tx.write({"acquirer_reference": mollie_response["id"]}) + + payment_url = mollie_response["links"]["paymentUrl"] + return werkzeug.utils.redirect(payment_url) + + return werkzeug.utils.redirect("/") diff --git a/data/mollie.xml b/data/mollie.xml new file mode 100755 index 0000000..096619b --- /dev/null +++ b/data/mollie.xml @@ -0,0 +1,28 @@ + + + + + + Mollie + + mollie + + + test + You will be redirected to the Mollie website after clicking on payment button.

]]>
+ mollie api test key + mollie api live key + + +

+ A payment gateway to accept online payments via various payment methods (check https://www.mollie.com/be/payments/). +

+
    +
  • eCommerce
  • +
+
+
+
+
+ diff --git a/images/main_screenshot.png b/images/main_screenshot.png new file mode 100755 index 0000000..186a936 Binary files /dev/null and b/images/main_screenshot.png differ diff --git a/images/odoo_configuration.png b/images/odoo_configuration.png new file mode 100644 index 0000000..19dd132 Binary files /dev/null and b/images/odoo_configuration.png differ diff --git a/images/payment_confirmation.png b/images/payment_confirmation.png new file mode 100644 index 0000000..217beeb Binary files /dev/null and b/images/payment_confirmation.png differ diff --git a/models/__init__.py b/models/__init__.py new file mode 100755 index 0000000..c0ba143 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +import mollie diff --git a/models/mollie.py b/models/mollie.py new file mode 100755 index 0000000..5ae73cb --- /dev/null +++ b/models/mollie.py @@ -0,0 +1,211 @@ +# -*- coding: utf-'8' "-*-" + +import json +import logging +from hashlib import sha256 +import urlparse +import unicodedata +import pprint +import requests + +from odoo import models, fields, api +from odoo.tools.float_utils import float_compare +from odoo.tools.translate import _ +from odoo.addons.payment.models.payment_acquirer import ValidationError +from odoo.tools.safe_eval import safe_eval + +_logger = logging.getLogger(__name__) + + +class AcquirerMollie(models.Model): + _inherit = 'payment.acquirer' + # Fields + + provider = fields.Selection(selection_add=[('mollie', 'Mollie')]) + mollie_api_key_test = fields.Char('Mollie Test API key', size=40, required_if_provider='mollie', groups='base.group_user') + mollie_api_key_prod = fields.Char('Mollie Live API key', size=40, required_if_provider='mollie', groups='base.group_user') + + @api.onchange('mollie_api_key_test') + def _onchange_mollie_api_key_test(self): + if self.mollie_api_key_test: + if not self.mollie_api_key_test[:5] == 'test_': + return {'warning': {'title': "Warning", 'message': "Value of Test API Key is not valid. Should begin with 'test_'",}} + + @api.onchange('mollie_api_key_prod') + def _onchange_mollie_api_key_prod(self): + if self.mollie_api_key_prod: + if not self.mollie_api_key_prod[:5] == 'live_': + return {'warning': {'title': "Warning", 'message': "Value of Live API Key is not valid. Should begin with 'live_'",}} + + def _get_mollie_api_keys(self, environment): + keys = {'prod': self.mollie_api_key_prod, + 'test': self.mollie_api_key_test + } + return {'mollie_api_key': keys.get(environment, keys['test']), } + + def _get_mollie_urls(self, environment): + """ Mollie URLS """ + url = { + 'prod': 'https://api.mollie.nl/v1/', + 'test': 'https://api.mollie.nl/v1/', } + + return {'mollie_form_url': url.get(environment, url['test']), } + + @api.multi + def mollie_form_generate_values(self, values): + self.ensure_one() + base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') + currency = self.env['res.currency'].sudo().browse(values['currency_id']) + language = values.get('partner_lang') + name = values.get('partner_name') + email = values.get('partner_email') + zip = values.get('partner_zip') + address = values.get('partner_address') + town = values.get('partner_city') + country = values.get('partner_country') and values.get('partner_country').code or '' + phone = values.get('partner_phone') + + amount = values['amount'] + mollie_key = getattr(self, 'id') + + mollie_tx_values = dict(values) + mollie_tx_values.update({ + 'OrderId': values['reference'], + 'Description': values['reference'], + 'Currency': currency.name, + 'Amount': amount, + 'Key': mollie_key, #self._get_mollie_api_keys(self.environment)['mollie_api_key'], + 'URL' : self._get_mollie_urls(self.environment)['mollie_form_url'], + 'BaseUrl': base_url, + 'Language': language, + 'Name': name, + 'Email': email, + 'Zip': zip, + 'Address': address, + 'Town': town, + 'Country': country, + 'Phone': phone + }) + + return mollie_tx_values + + @api.multi + def mollie_get_form_action_url(self): + self.ensure_one() + return "/payment/mollie/intermediate" + + +class TxMollie(models.Model): + _inherit = 'payment.transaction' + + @api.multi + def _mollie_form_get_tx_from_data(self, data): + reference = data.get('reference') + payment_tx = self.search([('reference', '=', reference)]) + + if not payment_tx or len(payment_tx) > 1: + error_msg = _('received data for reference %s') % (pprint.pformat(reference)) + if not payment_tx: + error_msg += _('; no order found') + else: + error_msg += _('; multiple order found') + _logger.info(error_msg) + raise ValidationError(error_msg) + + return payment_tx + + @api.multi + def _mollie_form_get_invalid_parameters(self, data): + invalid_parameters = [] + + return invalid_parameters + + @api.multi + def _mollie_form_validate(self, data): + reference = data.get('reference') + + acquirer = self.acquirer_id + + tx = self._mollie_form_get_tx_from_data(data) + + transactionId = tx['acquirer_reference'] + + _logger.info('Validated transfer payment for tx %s: set as pending' % (reference)) + mollie_api_key = acquirer._get_mollie_api_keys(acquirer.environment)['mollie_api_key'] + url = "%s/payments" % (acquirer._get_mollie_urls(acquirer.environment)['mollie_form_url']) + + payload = { + "id": transactionId + } + if acquirer.environment == 'test': + payload["testmode"] = True + + headers = {'content-type': 'application/json', 'Authorization': 'Bearer ' + mollie_api_key} + + mollie_response = requests.get( + url, data=json.dumps(payload), headers=headers).json() + + if self.state == 'done': + _logger.info('Mollie: trying to validate an already validated tx (ref %s)', reference) + return True + + data_list = mollie_response["data"] + data = {} + status = 'undefined' + mollie_reference = '' + if len(data_list) > 0: + data = data_list[0] + + if "status" in data: + status = data["status"] + if "id" in data: + mollie_reference = data["id"] + + if status == "paid": + vals = { + 'state': 'done', + 'date_validate': fields.datetime.strptime(data["paidDatetime"].replace(".0Z", ""), "%Y-%m-%dT%H:%M:%S"), + 'acquirer_reference': reference, + } + if mollie_reference and self.partner_id and self.type == 'form': + payment_token = self.env['payment.token'].create({ + 'partner_id': self.partner_id.id, + 'acquirer_id': self.acquirer_id.id, + 'acquirer_ref': mollie_reference, + 'name': data["method"] + }) + vals.update(payment_token_id=payment_token.id) + self.write(vals) + if self.callback_eval: + safe_eval(self.callback_eval, {'self': self}) + return True + elif status in ["cancelled", "expired", "failed"]: + self.write({ + 'state': 'cancel', + 'acquirer_reference': mollie_reference, + }) + return False + elif status in ["open", "pending"]: + self.write({ + 'state': 'pending', + 'acquirer_reference': mollie_reference, + }) + return False + else: + self.write({ + 'state': 'error', + 'acquirer_reference': mollie_reference, + }) + return False + +class PaymentToken(models.Model): + _inherit = 'payment.token' + + def mollie_create(self, values): + return { + 'acquirer_ref': values['acquirer_ref'], + 'name': values['name'] + } + + + diff --git a/static/description/crm_sc_01.png b/static/description/crm_sc_01.png new file mode 100755 index 0000000..186a936 Binary files /dev/null and b/static/description/crm_sc_01.png differ diff --git a/static/description/crm_sc_02.png b/static/description/crm_sc_02.png new file mode 100755 index 0000000..19dd132 Binary files /dev/null and b/static/description/crm_sc_02.png differ diff --git a/static/description/crm_sc_03.png b/static/description/crm_sc_03.png new file mode 100755 index 0000000..54c9e72 Binary files /dev/null and b/static/description/crm_sc_03.png differ diff --git a/static/description/crm_sc_04.png b/static/description/crm_sc_04.png new file mode 100755 index 0000000..44317c1 Binary files /dev/null and b/static/description/crm_sc_04.png differ diff --git a/static/description/crm_sc_05.png b/static/description/crm_sc_05.png new file mode 100755 index 0000000..217beeb Binary files /dev/null and b/static/description/crm_sc_05.png differ diff --git a/static/description/icon.png b/static/description/icon.png new file mode 100755 index 0000000..01e453e Binary files /dev/null and b/static/description/icon.png differ diff --git a/static/description/index.html b/static/description/index.html new file mode 100755 index 0000000..6a2c75a --- /dev/null +++ b/static/description/index.html @@ -0,0 +1,106 @@ +
+
+
+

Mollie Payment Acquirer

+
+
+
+ +
+
+
+

+Integrate Mollie in Odoo very easily. Mollie accepts the following payment methods :
+ +

    +
  • Creditcard
  • +
  • SOFORT Banking
  • +
  • iDEAL
  • +
  • Bancontact
  • +
  • Bank transfer
  • +
  • SEPA Direct Debit
  • +
  • PayPal
  • +
  • Bitcoin
  • +
  • PODIUM Cadeaukaart
  • +
  • paysafecard
  • +
  • KBC/CBC Payment Button
  • +
  • Belfius Direct Net
  • +
  • Gift cards
  • +
+

+
+
+
+ +
+
+

Easy install and configuration

+
+
+ +
+
+
+

+Install this module. Go to Invoicing section -> Payments -> Payment Acquirers -> Mollie and Add API Keys (test and/or live) from your Mollie Account.

+
+ +
+
+ +
+
+

After you choose to pay with Mollie, the possible payment methods will be displayed.

+
+

+When Mollie acquirer is configured correctly, you can see Mollie payment option at the time of checkout. +

+
+
+
+ +
+
+
+
+ +
+
+

Mollie handles the payment

+
+
+ +
+
+
+

+Once Mollie payment option is selected, it will redirect the shopper to all payment methods supported by Mollie. +

+
+
+
+ +
+
+

Confirmation

+
+

+After a succesfull payment, a confirmation is shown to the shopper. +

+
+
+
+ +
+
+
+
+ +
+

For any questions, support and development

+

+ +  Contact Us  +

+
+ diff --git a/static/src/img/mollie_icon.png b/static/src/img/mollie_icon.png new file mode 100755 index 0000000..01e453e Binary files /dev/null and b/static/src/img/mollie_icon.png differ diff --git a/static/src/img/mollie_logo.png b/static/src/img/mollie_logo.png new file mode 100755 index 0000000..01e453e Binary files /dev/null and b/static/src/img/mollie_logo.png differ diff --git a/views/mollie.xml b/views/mollie.xml new file mode 100755 index 0000000..293da01 --- /dev/null +++ b/views/mollie.xml @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/views/payment_acquirer.xml b/views/payment_acquirer.xml new file mode 100755 index 0000000..8f40fe6 --- /dev/null +++ b/views/payment_acquirer.xml @@ -0,0 +1,20 @@ + + + + + + acquirer.form.mollie + payment.acquirer + + + + + + + + + + + + +