Skip to content

Commit

Permalink
Mollie plugin for Odoo
Browse files Browse the repository at this point in the history
  • Loading branch information
Martijn Smit committed Oct 3, 2017
0 parents commit 5f23073
Show file tree
Hide file tree
Showing 22 changed files with 548 additions and 0 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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")
2 changes: 2 additions & 0 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import models
import controllers
18 changes: 18 additions & 0 deletions __manifest__.py
Original file line number Diff line number Diff line change
@@ -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']
}
3 changes: 3 additions & 0 deletions controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-

import main
105 changes: 105 additions & 0 deletions controllers/main.py
Original file line number Diff line number Diff line change
@@ -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("/")
28 changes: 28 additions & 0 deletions data/mollie.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">

<record id="payment.payment_acquirer_mollie" model="payment.acquirer">
<field name="name">Mollie</field>
<field name="image" type="base64" file="payment_mollie_beopen/static/src/img/mollie_icon.png"/>
<field name="provider">mollie</field>
<field name="company_id" ref="base.main_company"/>
<field name="view_template_id" ref="payment_mollie_beopen.mollie_acquirer_button"/>
<field name="environment">test</field>
<field name="pre_msg"><![CDATA[
<p>You will be redirected to the Mollie website after clicking on payment button.</p>]]></field>
<field name="mollie_api_key_test">mollie api test key</field>
<field name="mollie_api_key_prod">mollie api live key</field>
<field name="module_id" ref="base.module_payment_mollie_beopen"/>
<field name="description" type="html">
<p>
A payment gateway to accept online payments via various payment methods (check https://www.mollie.com/be/payments/).
</p>
<ul>
<li><i class="fa fa-check"/>eCommerce</li>
</ul>
</field>
</record>
</data>
</odoo>

Binary file added images/main_screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/odoo_configuration.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/payment_confirmation.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-

import mollie
211 changes: 211 additions & 0 deletions models/mollie.py
Original file line number Diff line number Diff line change
@@ -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']
}



Binary file added static/description/crm_sc_01.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/description/crm_sc_02.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/description/crm_sc_03.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/description/crm_sc_04.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/description/crm_sc_05.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/description/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 5f23073

Please sign in to comment.