Midtrans Payment Gateway Integration with Odoo 13

Galih Fikran
7 min readNov 27, 2022

--

Odoo eCommerce

Odoo memiliki banyak sekali modul ERP yang bisa kita manfaatkan salah satunya yaitu eCommerce. eCommerce adalah sebuah platform yang menangani transaksi atau jual beli dengan menggunakan jaringan internet ataupun peralatan elektronik lainnya. Contoh nyata dari yang sering kita jumpai sehari-hari ialah marketplace seperti shopee dan tokopedia, dalam hal ini shopee dan tokopedia adalah platform digital yang menjembatani transaksi antara penjual dan pembeli, dengan begitu komunikasi yang terjalin antara keudanya berlangsung secara elektronik/digital. Salah satu mekanisme yang sering kita lakukan adalah pembayaran online, walaupun memang saat ini ada juga metode pembayaran Cash On Delivery atau biasa disebut COD namun hal ini juga akan tersistemasi dan tertulis secara elektronik pada sebuah sistem aplikasi.

Payment Gateway

Selanjutnya apakah anda pernah mendengar mengenai Payment Gateway ?. Payment gateway merupakan salah satu Fintech, teknologi keuangan/Fintech merupakan hasil kombinasi antara layanan keuangan dengan teknologi yang akhirnya mengubah model bisnis dari konvensional menjadi moderat. Salah satu payment gateway yang kita kenal dan sering di pakai khususnya di Indonesia adalah Midtrans. Midtrans menyediakan berbagai payment method atau cara pembayaran seperti Card Credit, Bank Transfer, E-money dan masih ada banyak lagi. Tren pembayaran dengan menggunakan payment gateway akan berpotensi mempengaruhi pola perilaku seseorang.

Payment Process Flow

Saat customer melakukan checkout pada produk yang ingin dia beli maka yang harus dia lakukan selanjutnya adalah menentukan cara pembayaran yang diinginkan, satu lagi yang tidak boleh ketinggakan adalah mengecek voucher jika ada hehe.. Actually detail transaksi dikirimkan sebelum aplikasi menampilkan payment method yang tersedia kemudian token yang dihasilkan dari detail transaksi digunakan untuk proses selanjutnya. dari token yang dihasilkan digunakan untuk menampilkan snap yang berisi daftar payment method, disini customer akan memilih payment method yang diinginkan. Pada proses ini sistem akan memberikan detail transaksi kepada midtrans beserta status pembayaran, nah selanjutnya untuk menangani perubahan status yang bisa saja berubah sewaktu-waktu dikarenakan kita tidak pernah tahu kapan customer akan benar-benar membayar, maka diperlukan webhook. Webhook berguna untuk menangkap notifikasi yang dikirimkan dari midtrans ketika ada perubahan state pada suatu transaksi.

Tulisan kali ini akan membahas bagaimana cara mengintegrasikan salah satu payment gateway yaitu midtrans dengan aplikasi eCommerce di Odoo. Sebagai disclaimer odoo yang saya pakai adalah versi 13 jadi mungkin saja ada beberapa metode yang saya terapkan tidak berjalan di environment anda dikarenakan perbedaan versi.

Untuk dapat mengikuti tulisan ini, saya asumsikan anda adalah orang yang sudah memiliki basic pengetahuan tentang odoo terutama pada segi technical-nya. Tulisan ini tidak akan menjelaskan secara mendetail, setiap langkah-langkahnya akan ditulis sederhana dan ringkas.

Setelah membaca tulisan ini diharapkan pembaca akan mengetahui beberapa hal berikut ini:

  1. Dapat memahami peran payment gateway pada proses transaksi antara penjual dan pembeli
  2. Dapat melakukan konfigurasi payment acquirer di odoo 13
  3. Dapat menambahkan payment acquirer baru di odoo 13
  4. Dapat melakukan integrasi midtrans di odoo 13

Code Implementation

Langkah pertama buatlah file __manifest__.py

{'name': 'Midtrans Payment Acquirer','category': 'Accounting/Payment Acquirers','version': '1.0','sequence': 343,'summary': 'Payment Acquirer: Midtrans Implementation','description': """Midtrans Payment Acquirer""",'depends': ['payment'],'data': ['views/payment_midtrans_template.xml','views/midtrans_view.xml','data/payment_acquirer_data.xml','views/assets.xml',],'application': True,'uninstall_hook': 'uninstall_hook','license': 'LGPL-3',}

Pastikan anda memasukkan module payment kedalam depends. Abaikan data files terlebih dahulu. Setelah itu silahkan update dan install module yang baru saja dibuat.

Selanjutnya tambahkan record midtrans payment gateway di database odoo, untuk membuatnya terlebih dahulu buat file data/payment_acquirer_data.xml. Kemudian perhatikan kode dibawah ini:

<?xml version="1.0" encoding="utf-8"?>
<odoo>

<record id="payment_acquirer_midtrans" model="payment.acquirer">
<field name="name">Midtrans</field>
<field name="provider">midtrans</field>
<field name="view_template_id"
ref="payment.default_acquirer_button"/>
<field name="display_as">Payment Midtrans</field>
<field name="image_128" type="base64"
file="payment_midtrans/static/src/img/midtrans_icon.png"/>
<field name="module_id" ref="base.module_payment_midtrans"/>
<field name="description" type="html">
<p>
A payment gateway to accept
online payments via credit cards,
debit cards and bank transfers.
</p>
<ul class="list-inline">
<li class="list-inline-item"><i class="fa fa-check"/>
Online Payment</li>
<li class="list-inline-item"><i class="fa fa-check"/>
Payment Status Tracking</li>
</ul>
</field>
</record>

</odoo>

view_template_id diisi id form yang akan muncul ketika payment gatewat dipilih. Karena pada case ini saya menggunakan snap maka view_template_id diisi payment.default_acquirer_button, ini bertujuan supaya tidak akan muncul form apapun ketika payment gateway dipilih, jadi kita bisa memanfaatkan javascript untuk menampilkan snap. Untuk field yang lain saya asumsikan anda sudah memahaminya.

Buat model untuk menyimpan server_key dan client_key.

class PaymentAcquirer(models.Model):
_inherit = 'payment.acquirer'

provider = fields.Selection(selection_add=[('midtrans', 'Midtrans')])
merchant_code = fields.Char(string="Merchant Code")

server_key = fields.Char(string="Server Key", required=True)
client_key = fields.Char(string="Client Key", required=True)

def get_payment_acquirer_state(self):
return self.state

@api.onchange('state')
def on_change_state(self):
self.server_key = ''
self.client_key = ''

Langkah selanjutnya buatlah view untuk menerima server key dan client key. Di Odoo terdapat 3 state untuk payment gateway yaitu Disabled, Enabled, Test Mode. Maka dari itu anda harus menyesuaikan key mana yang akan digunakan ketika mode production dan mode test. Buat view tersebut di dalam folder views, dan jangan lupa masukan kedalam manifest. Anda bisa melihat contoh dibawah ini:

<record id="payment_acquirer_view_form_inherit_payment_midtrans" 
model="ir.ui.view">
<field name="name">
payment.acquirer.view.form.inherit.payment.midtrans
</field>
<field name="model">payment.acquirer</field>
<field name="inherit_id" ref="payment.acquirer_form"/>
<field name="arch" type="xml">
<xpath expr='//group[@name="acquirer"]' position='inside'>
<group attrs="{'invisible':
[('provider', '!=', 'midtrans')]}">
<field name="merchant_code" attrs="{'required':
[
('provider', '=', 'midtrans'),
('state', '!=', 'disabled')
]
}"/>
<field name="server_key" attrs="{'required':
[
('provider', '=', 'midtrans'),
('state', '!=', 'disabled')
]
}" password="True"/>
<field name="client_key" attrs="{'required':
[
('provider', '=', 'midtrans'),
('state', '!=', 'disabled')
]
}" password="True"/>
</group>
</xpath>
</field>
</record>

Ada tiga field yang dibutuhkan, yaitu merchant_code, server_key , dan client_key anda harus menambahkan domain pada setiap field suapaya ketika payment gateway adalah midtrans dan state tidak sama dengan disabled maka field tersebut harus diisi.

Pada javascript ada satu modul yang harus dimodifikasi yaitu PaymentForm, import payment.payment_form dan web.ajax. web.ajax digunakan untuk load javascript file yang dibutuhkan. Ada dua function yang harus di override dan satu function user defined. Untuk melakukan override function gunakan keyword include. Function loadSnapUrl bertujuan untuk melakukan load file javascript yang dibutuhkan untuk memunculkan snap. Midtrans memiliki url yang berbeda untuk production dan test mode, maka sebelumnya adalah melakukan pengecekan state pada database. Dibawah ini saya menggunakan metode rpc untuk memastikan state dari payment acquirer.

loadSnapUrl: function (acquirerId) {
this._rpc({
model: 'payment.acquirer',
method: 'get_payment_acquirer_state',
args: [parseInt(acquirerId)]
}).then(function (result) {
if (result == 'enabled') {
return ajax.loadJS("https://
app.midtrans.com/snap/snap.js");
} else {
return ajax.loadJS("https://
app.sandbox.midtrans.com/snap/snap.js");
}
});
},

RPC digunakan untuk melakukan pengambilan data dari database, sistem kerjanya mirip dengan ajax pada jquery atau XMLHttpRequest pada javascript native.

Selanjutnya modifikasi function willStart, fungsi inilah yang akan memanggil loadSnapUrl dengan mengirimkan acquirer id. Acquirer id didapatkan dengan melakukan pencarian pada elemen html menggunakan teknik DOM, sebenarnya cara ini tidak direkomendasikan.

willStart: function () {
var acquirerId;
var self = this;

return this._super.apply(this, arguments)
.then(function () {
$('.o_payment_acquirer_select')
.on('click', function(e) {
acquirerId = this.querySelector('input[type="radio"]')
.dataset.acquirerId;
self.loadSnapUrl(acquirerId);
});

$.each($('.o_payment_acquirer_select')
.find('input'), function(index, value) {
if (value.checked) {
acquirerId = value.dataset.acquirerId;
self.loadSnapUrl(acquirerId);
}
});
});

},

Untuk membuat detail transaksi anda perlu membuat model untuk menyimpan data tersebut. Seharusnya model bisa saja dibuat transient karena data tersebut sebenarnya data temporary artinya hanya diperlukan pada saat proses pembayaran. Data ini nantinya akan dikirimkan ke midtrans, setelah dikirmkan maka anda akan mendapatkan token untuk membuka snap.

class TxMidtrans(models.Model):
_inherit = 'payment.transaction'

token_transaction = fields.Char()
redirect_url = fields.Char()

def payment_midtrans_transaction(self):
"""
create detail transaction
"""

last_tx_id = request.session.get('__website_sale_last_tx_id')
midtrans = self.env['payment.acquirer']
.search([('provider', '=', 'midtrans')])

snap = midtransclient.Snap(
is_production = False if midtrans.state == 'test' else True,
server_key = midtrans.server_key,
client_key = midtrans.client_key,
)

payment_transaction = self.env['payment.transaction']
.browse(last_tx_id)

letters = string.ascii_letters
order_id = ''.join(random.choice(letters)
for i in range(10))+':'+payment_transaction.reference

param = {
"transaction_details": {
"order_id": order_id,
"gross_amount": int(payment_transaction.amount),
"currency": "IDR",
}, "credit_card":{
"secure" : True
},
"customer_details": {
"first_name": _partner_split_name
(payment_transaction.partner_name)[0],
"last_name": _partner_split_name
(payment_transaction.partner_name)[1],
"email": payment_transaction.partner_email,
"phone": payment_transaction.partner_phone
}
}

transaction = snap.create_transaction(param)

self.token_transaction = transaction['token']
self.redirect_url = transaction['redirect_url']

return transaction

Lihat dan baca dokumentasi midtrans terkait data apa saja yang harus dikirim. Pengiriman data menggunakan metode snap.create_transaction(). Sebelumnya import library midtransclient untuk dapat menggunakan metode tersebut.

Selanjutnya buatlah aksi ketika payment button di tekan oleh user. Lakukan override ke payEvent function di base odoo. payEvent mengembalikan promise, jadi kita bisa memanfaatkanya dengan melakukan chaining dengan method then kemudian tulis isi code disitu. Kita akan menambahkan proses setelah detail transaksi dikirim lalu fungsi ini bertujuan untuk mengambil token dan menampilkan snap.

payEvent: function (ev) {
var self = this;
this._super.apply(this, arguments).then( (values) => {
var $checkedRadio = this.$('input[type="radio"]:checked');

if ($checkedRadio.length === 1
&& this.isFormPaymentRadio($checkedRadio)
&& $checkedRadio.data('provider') === 'midtrans') {
self._rpc({
model: 'payment.transaction',
method: 'payment_midtrans_transaction',
args: [[]]
}).then(function(result) {
var token = result.token;

snap.pay(token, {
onSuccess: function(){
window.location = '/shop/confirmation';
},
onPending: function(){
self.do_warn('Warning',
'Wating your payment!');
window.location = '/shop/confirmation';
},
onError: function(){
self.do_warn('Error', 'Payment Failed!');
window.location = '/shop/confirmation';
},
onClose: function(){
self.do_warn('Warning',
'You closed the popup without
finishing the payment');
window.location = '/shop/payment';
}
});

});
}
});
},

});

Langkah terakhir adalah membuat Webhook. Buat sebuah route kemudian daftarkan route tersebut di dashboard midtrans sehingga midtrans dapat mengenali route yang mengarah ke webhook aplikasi anda.

class PayMidtransController(http.Controller):
@http.route('/payment/midtrans/webhook', type='json',
auth='public', methods=['POST'], csrf=False)
def midtrans_webhook(self, **data):
"""
to catch response from midtrans notification of payment
"""
midtrans = request.env['payment.acquirer']
.search([('provider', '=', 'midtrans')])

api_client = midtransclient.CoreApi(
is_production = False if midtrans.state == 'test' else True,
server_key = midtrans.server_key,
client_key = midtrans.client_key,
)

data = json.loads(request.httprequest.data)

status_response = api_client.transactions.notification(data)

order_id = status_response['order_id']
transaction_status = status_response['transaction_status']

reference = order_id.split(':')[1]
payment_tx = http.request.env['payment.transaction']
.sudo().search([('reference', '=', reference)])

if transaction_status == 'settlement':
payment_tx.state = 'done'
elif transaction_status == 'pending':
payment_tx.state = 'pending'
elif transaction_status == 'cancel'
or transaction_status == 'deny'
or transaction_status == 'expire':
payment_tx.state = 'cancel'

return http.Response(status=200)

Anda sudah berhasil melakukan integrasi midtrans dengan Odoo, selanjutnya lakukanlah improvement lain. Untuk mendapatkan full code silahkan hubungi email berikut : galih11120@gmail.com

--

--

Galih Fikran
Galih Fikran

Written by Galih Fikran

Passionate in technology, computer science, and mathematical statistics