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/).
+
+
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+
+
+
+