Integrasi Payment Gateway Xendit
Odoo 16

Bismillah,
Assalamu'alaikum Warohmatullah Wabarokatuh,

Di artikel kali ini, insyaAllah saya akan memberikan sedikit tips bagaimana cara melakukan integrasi payment gateway menggunakan xendit.

Studi kasusnya adalah ketika saya membuat customer invoice dan mempostingnya, maka akan ada button untuk men-generate payment link dari xendit. Jika customr sudah melakukan pembayaran maka otomatis di odoo akan melakukan register payment atas invoice yang sudah dibayar.

Berikut langkah - langkahnya :

1. Pastikan sudah memiliki akun di xendit, jika belum silahkan lakukan registrasi di https://dashboard.xendit.co/register/

2. Buat API Key di portal xendit untuk bisa melakukan integrasi (Settings > API Keys)


3. Buat Config untuk menampung API Keys, Payment Journal dan URL Callback

di file python res_config_settings.py :

# -*- coding: utf-8 -*-

from odoo import models, fields, api
import requests
import json
from datetime import date, datetime, time, timedelta
from odoo.exceptions import ValidationError, UserError

class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'

xendit_api_key = fields.Char(string="API Key", config_parameter="aqn_xendit_ext.xendit_api_key")
xendit_journal_id = fields.Many2one('account.journal', string="Payment Journal", config_parameter="aqn_xendit_ext.xendit_journal_id")
xendit_url_callback = fields.Char(string="URL Callback", config_parameter="aqn_xendit_ext.xendit_url_callback")
def get_xendit_config_api(self, param, check_exist=False):
params = {
'xendit_api_key': self.env['ir.config_parameter'].sudo().get_param('aqn_xendit_ext.xendit_api_key'),
'xendit_journal_id': self.env['ir.config_parameter'].sudo().get_param('aqn_xendit_ext.xendit_journal_id'),
'xendit_url_callback': self.env['ir.config_parameter'].sudo().get_param('aqn_xendit_ext.xendit_url_callback'),
}

if param == 'xendit_api_key':
if not params.get('xendit_api_key') and not check_exist:
raise ValidationError('API Key belum diisi di Settings (Xendit API)')
elif param == 'xendit_journal_id':
if not params.get('xendit_journal_id') and not check_exist:
raise ValidationError('Journal belum diisi di Settings (Xendit API)')
elif param == 'xendit_url_callback':
if not params.get('xendit_url_callback') and not check_exist:
raise ValidationError('URL Callback belum diisi di Settings (Xendit API)')

if param:
if param == 'xendit_journal_id':
return int(params.get(param))
else:
return params.get(param)


di file xml res_config_settings_views.xml :

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

<record id="res_config_settings_view_form_inherit_xendit" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.xendit</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base_setup.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('settings')]" position="inside">
<div class="app_settings_block" data-string="Xendit API" string="Xendit API" data-key="aqn_xendit_ext">
<h2>Xendit API</h2>
<div class="row mt16 o_settings_container" id="xendit_setting_container">
<div class="col-12 col-lg-6 o_setting_box" id="xendit_configuration">
<div class="content-group">
<group>
<field name="xendit_api_key"/>
<field name="xendit_journal_id" options="{'no_create':1}"/>
<field name="xendit_url_callback"/>
</group>
</div>
</div>
</div>
</div>
</xpath>
</field>

</record>
</data>
</odoo>



4. Install library xendit untuk python

$ pip3 install xendit-python

untuk dokumentasi library ini bisa dicek di https://github.com/xendit/xendit-python/blob/master/docs/InvoiceApi.md

5. di object account.move untuk customer invoice tambahkan field payment link, xendit invoice id, payment token (untuk verifikasi callback) dan buat fungsi generate payment link xendit

# -*- coding: utf-8 -*-

from odoo import models, fields, api
from odoo.exceptions import UserError, RedirectWarning, ValidationError
import requests
import json
import base64
import io
# from xendit import Xendit
import uuid
import time
import xendit
from xendit.apis import InvoiceApi
from xendit.invoice.model.invoice_not_found_error import InvoiceNotFoundError
from xendit.invoice.model.invoice import Invoice
from xendit.invoice.model.bad_request_error import BadRequestError
from xendit.invoice.model.unauthorized_error import UnauthorizedError
from xendit.invoice.model.forbidden_error import ForbiddenError
from xendit.invoice.model.create_invoice_request import CreateInvoiceRequest
from xendit.invoice.model.customer_object import CustomerObject
from xendit.invoice.model.address_object import AddressObject
from xendit.invoice.model.notification_preference import NotificationPreference
from xendit.invoice.model.notification_channel import NotificationChannel
from xendit.invoice.model.invoice_item import InvoiceItem
from xendit.invoice.model.invoice_fee import InvoiceFee

from pprint import pprint

class AccountMove(models.Model):
_inherit = 'account.move'

xendit_payment_link = fields.Char('Payment Link', copy=False)
xendit_invoice_id = fields.Char('Xendit Invoice ID', copy=False)
xendit_payment_token = fields.Char('Xendit Payment Token')


def create_payment(self, move_id):
obj_config = self.env['res.config.settings']
obj_move = self.env['account.move']
obj_pym = self.env['account.payment']
move = obj_move.browse(move_id)
journal_id = obj_config.get_xendit_config_api('xendit_journal_id')
payment = obj_pym.create({
'amount': move.amount_residual, # move.amount_total,
'payment_type': 'inbound',
'ref': move.payment_reference,
'partner_id': move.partner_id.id,
'partner_type': 'customer',
'journal_id': journal_id,
})
payment.action_post()

# Reconcile
ml_to_recon = self.env['account.move.line']
# Jurnal Invoice
if move.line_ids:
for ml in move.line_ids:
if ml.account_id.id == move.partner_id.property_account_receivable_id.id:
ml_to_recon += ml
# Jurnal Payment
if payment.line_ids:
for ml in payment.line_ids:
if ml.account_id.id == payment.destination_account_id.id:
ml_to_recon += ml
if ml_to_recon:
ml_to_recon.reconcile()

def rpc_register_payment(self, move_id=False, token=False):
obj_move = self.env['account.move']
move = obj_move.browse(move_id)
if move_id:
if move.xendit_payment_token != token:
return {
'error': True,
'message': f'Token tidak sesuai',
'data': {}
}
self.create_payment(move_id)
return {
'error': False,
'message': f'Pembayaran invoice {move.name} berhasil ditambahkan',
'data': {}
}
return {
'error': True,
'message': f'Invoice dengan ID {move.id} tidak ditemukan',
'data': {}
}
def remote_xendit_payment_link(self):
obj_config = self.env['res.config.settings']
obj_journal = self.env['account.journal']

api_key = obj_config.get_xendit_config_api('xendit_api_key')


xendit.set_api_key(api_key)
api_client = xendit.ApiClient()
api_instance = InvoiceApi(api_client)

for rec in self:

items = []

for item in rec.invoice_line_ids.filtered(lambda x: x.display_type == 'product'):
items.append(
InvoiceItem(
name=item.name,
price=item.price_unit,
quantity=item.quantity,
# reference_id="reference_id_example",
# url="url_example",
# category=item.product_id.categ_id.name,
),
)

if not rec.partner_id.email:
raise ValidationError(f'Email partner {rec.partner_id.name} belum dilengkapi!')
payment_method = ["CREDIT_CARD"]

if not rec.partner_id.country_id:
raise ValidationError(f"Negara untuk partner {rec.partner_id.name} belum dilengkapi!")

if rec.partner_id.country_id.code == 'ID':
# journal_id = obj_config.get_xendit_config_api('xendit_journal_id')
# journal = obj_journal.browse(journal_id)
# if not journal.xendit_bank_code:
# raise ValidationError(f'Xendit Bank Code belum dilengkapi di jurnal pembayaran {journal.name}')
payment_method = ['BCA', 'BNI', 'BSI', 'BRI', 'MANDIRI', 'PERMATA']
url_callback = obj_config.get_xendit_config_api('xendit_url_callback')

token = str(uuid.uuid4())

duration = 0
aging = rec.invoice_date_due - rec.invoice_date
if aging.days == 0:
duration = 24 * 3600
elif aging.days > 365:
duration = 365 * 24 * 3600
else:
duration = aging.days * 24 * 3600

create_invoice_request = CreateInvoiceRequest(
external_id= rec.name,
amount=rec.amount_residual,
payer_email= rec.partner_id.email,
description= rec.payment_reference,
invoice_duration=str(duration),
# callback_virtual_account_id="callback_virtual_account_id_example",
# should_send_email=True,
customer= CustomerObject(
# id=rec.partner_id.id,
phone_number=rec.partner_id.mobile or rec.partner_id.phone or "",
given_names=rec.partner_id.name,
surname=rec.partner_id.name,
email=rec.partner_id.email,
mobile_number=rec.partner_id.mobile or "",
# customer_id=str(rec.partner_id.id),
addresses=[
AddressObject(
country=rec.partner_id.country_id.name or "",
street_line1=rec.partner_id.street or "",
street_line2=rec.partner_id.street2 or "",
city=rec.partner_id.city or "",
province=rec.partner_id.state_id.name or "",
# state=rec.partner_id.state_id.name,
postal_code=rec.partner_id.zip or "",
),
],
),
customer_notification_preference= NotificationPreference(
invoice_created=[
NotificationChannel("email"),
NotificationChannel("whatsapp"),
],
# invoice_reminder=[
# NotificationChannel("email"),
# ],
# invoice_expired=[
# NotificationChannel("email"),
# ],
invoice_paid=[
NotificationChannel("email"),
NotificationChannel("whatsapp"),
],
),
success_redirect_url=f"{url_callback}?move_id={rec.id}&token={token}",
# failure_redirect_url="failure_redirect_url_example",
payment_methods=payment_method,
# mid_label="mid_label_example",
# should_authenticate_credit_card=True,
currency=rec.currency_id.name,
# reminder_time=3.14,
# local="local_example",
# reminder_time_unit="reminder_time_unit_example",
items=items
# fees=[
# InvoiceFee(
# type="type_example",
# value=3.14,
# ),
# ],
) # CreateInvoiceRequest
# for_user_id = "62efe4c33e45694d63f585f8" # str | Business ID of the sub-account merchant (XP feature)

# example passing only required values which don't have defaults set
try:
# Create an invoice
api_response = api_instance.create_invoice(create_invoice_request)
pprint(api_response)
if api_response.invoice_url:
rec.xendit_payment_link = api_response.invoice_url
rec.xendit_invoice_id = api_response.id
rec.xendit_payment_token = token

except xendit.XenditSdkException as e:
raise ValidationError("Exception when calling InvoiceApi->create_invoice: %s\n" % e)


6. munculkan di view customer invoice

<odoo>
<data>
<record id="view_move_form_inherit_xendit" model="ir.ui.view">
<field name="name">account.move.inherit</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<button name='action_register_payment' position="after">
<!-- <button name="remote_payment_request_xendit" type="object" string="Payment Xendit"/> -->
<!-- <button name="remote_xendit_create_va" type="object" string="Create VA"/> -->
<button name="remote_xendit_payment_link" type="object" string="Create Payment Link" attrs="{'invisible':['|',('state','!=','posted'),('move_type','!=','out_invoice')]}"/>

</button>
<field name="invoice_vendor_bill_id" position="after">
<field name="xendit_payment_link" readonly="1" widget="CopyClipboardChar" attrs="{'invisible':[('xendit_payment_link','=',False)]}"/>
<!-- <field name="xendit_invoice_id" readonly="1" attrs="{'invisible':[('xendit_invoice_id','=',False)]}"/> -->
</field>
</field>
</record>

</data>
</odoo>



7. Buat controller untuk url callback yang akan dipanggil ketika pembayaran dari xendit berhasil dilakukan

# -*- coding: utf-8 -*-
from odoo import http
import uuid


class AqnXenditExt(http.Controller):
@http.route('/xendit_callback_payment', auth='public')
def index(self, **kw):
account_move = http.request.env['account.move']
if kw.get('move_id'):
result = account_move.sudo().rpc_register_payment(int(kw.get('move_id')), kw.get('token'))
return str(result)



Terima kasih, semoga bermanfaat.
Kahfi
Wassalamu'alaikum Warohmatullah Wabarokatuh




Cara mendapatkan nilai SUM menggunakan ORM odoo