[ADD] pos_paytm: add paytm pos terminal payment method

This commit adds the module pos_paytm to add a new payment method of payTM
to point_of_sale.

task - 3377992

closes odoo/odoo#134960

X-original-commit: 2630c3e5f451df3b86f4906f7dfa3e5e1f5ec199
Signed-off-by: Harsh Modi (hamo) <hamo@odoo.com>
This commit is contained in:
Harsh Modi
2023-09-11 11:53:55 +00:00
parent f3614a4b5b
commit 3f5da1d9ea
13 changed files with 491 additions and 0 deletions
@@ -35,6 +35,7 @@ class ResConfigSettings(models.TransientModel):
module_pos_adyen = fields.Boolean(string="Adyen Payment Terminal", help="The transactions are processed by Adyen. Set your Adyen credentials on the related payment method.")
module_pos_stripe = fields.Boolean(string="Stripe Payment Terminal", help="The transactions are processed by Stripe. Set your Stripe credentials on the related payment method.")
module_pos_six = fields.Boolean(string="Six Payment Terminal", help="The transactions are processed by Six. Set the IP address of the terminal on the related payment method.")
module_pos_paytm = fields.Boolean(string="PayTM Payment Terminal", help="The transactions are processed by PayTM. Set your PayTM credentials on the related payment method.")
module_pos_preparation_display = fields.Boolean(string="Preparation Display", help="Show orders on the preparation display screen.")
update_stock_quantities = fields.Selection(related="company_id.point_of_sale_update_stock_quantities", readonly=False)
account_default_pos_receivable_account_id = fields.Many2one(string='Default Account Receivable (PoS)', related='company_id.account_default_pos_receivable_account_id', readonly=False)
@@ -300,6 +300,9 @@
<setting title="The transactions are processed by Six. Set the IP address of the terminal on the related payment method." string="Six" documentation="/applications/sales/point_of_sale/payment_methods/terminals/six.html" help="Accept payments with a Six payment terminal">
<field name="module_pos_six"/>
</setting>
<setting title="The transactions are processed by PayTM. Set your PayTM credentials on the related payment method." string="PayTM" help="Accept payments with a PayTM payment terminal">
<field name="module_pos_paytm"/>
</setting>
</block>
<block title="Connected Devices" id="pos_connected_devices_section">
+1
View File
@@ -0,0 +1 @@
from . import models
+32
View File
@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'POS PayTM',
'version': '1.0',
'category': 'Sales/Point of Sale',
'sequence': 6,
'summary': 'Integrate your POS with a PayTM payment terminal',
'description': """
Allow Paytm POS payments
==============================
This module allows customers to pay for their orders with debit/credit
cards and UPI. The transactions are processed by Paytm POS. A Paytm merchant account is necessary. It allows the
following:
* Fast payment by just swiping/scanning a credit/debit card or a QR code while on the payment screen
* Supported cards: Visa, MasterCard, Rupay, UPI
""",
'data': [
'views/pos_payment_method_views.xml',
'views/pos_payment_views.xml',
],
'depends': ['point_of_sale'],
'installable': True,
'assets': {
'point_of_sale._assets_pos': [
'pos_paytm/static/**/*',
],
},
'license': 'LGPL-3',
}
+2
View File
@@ -0,0 +1,2 @@
from . import pos_payment_method
from . import pos_payment
+32
View File
@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models, fields
class PosPayment(models.Model):
_inherit = "pos.payment"
paytm_authcode = fields.Char('Paytm APPR Code')
paytm_issuer_card_no = fields.Char('Paytm Issue Mask Card No.')
paytm_issuer_bank = fields.Char('Paytm Issuer Bank')
paytm_payment_method = fields.Char('Paytm Payment Method')
paytm_reference_no = fields.Char('Paytm Merchant Reference No.')
paytm_card_scheme = fields.Char('Paytm Card Scheme')
class PosOrder(models.Model):
_inherit = 'pos.order'
@api.model
def _payment_fields(self, order, ui_paymentline):
fields = super()._payment_fields(order, ui_paymentline)
fields.update({
'paytm_authcode': ui_paymentline.get('paytm_authcode'),
'paytm_issuer_card_no': ui_paymentline.get('paytm_issuer_card_no'),
'paytm_issuer_bank': ui_paymentline.get('paytm_issuer_bank'),
'paytm_payment_method':ui_paymentline.get('paytm_payment_method'),
'paytm_reference_no': ui_paymentline.get('paytm_reference_no'),
'paytm_card_scheme': ui_paymentline.get('paytm_card_scheme')
})
return fields
@@ -0,0 +1,166 @@
# coding: utf-8
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
import hashlib
import logging
import requests
import secrets
import string
from odoo.exceptions import UserError
from odoo import fields, models, api, _
from datetime import datetime
from dateutil import tz
_logger = logging.getLogger(__name__)
REQUEST_TIMEOUT = 30
iv = b'@@@@&&&&####$$$$'
class PosPaymentMethod(models.Model):
_inherit = 'pos.payment.method'
paytm_tid = fields.Char(string='PayTM Terminal ID')
channel_id = fields.Char(string='PayTM Channel ID', default='EDC')
accept_payment = fields.Selection(selection=[('auto', 'Automatically'), ('manual', 'Manually')], default='auto')
allowed_payment_modes = fields.Selection(selection=[('all', 'All'), ('card', 'Card'), ('qr', 'QR')], default='all')
paytm_mid = fields.Char(string="PayTM Merchant ID")
paytm_merchant_key = fields.Char(string="PayTM Merchant API Key")
paytm_test_mode = fields.Boolean(string="PayTM Test Mode", default=False)
def _get_payment_terminal_selection(self):
return super()._get_payment_terminal_selection() + [('paytm', 'PayTM')]
def _paytm_make_request(self, url, payload=None):
""" Make a request to PayTM API.
:param str url: The url to be reached by the request.
:param dict payload: The payload of the request.
:return The JSON-formatted content of the response.
:rtype: dict
"""
try:
if self.paytm_test_mode:
api_url = 'https://securegw-stage.paytm.in/ecr/'
else:
api_url = 'https://securegw-edc.paytm.in/ecr/'
response = requests.post(api_url+url, json=payload, timeout=REQUEST_TIMEOUT)
response.raise_for_status()
except (requests.exceptions.Timeout, requests.exceptions.RequestException) as error:
_logger.warning("Cannot connect with PayTM. Error: %s", error)
return {'error': '%s' % error}
res_json = response.json()
if res_json.get('body'):
return res_json['body']
default_error_msg = _('Something went wrong with paytm request. Please try later.')
error = res_json.get('error') or default_error_msg
return {'error': '%s' % error}
def paytm_make_payment_request(self, amount, transaction_id, reference_id, timestamp):
body = self._paytm_get_request_body(transaction_id, reference_id, timestamp)
body['transactionAmount'] = str(int(amount))
if self.accept_payment == 'auto':
body['autoAccept'] = 'True'
body['paymentMode'] = self.allowed_payment_modes.upper()
head = self._paytm_get_request_head(body)
head_error = head.get('error')
if head_error:
return {'error': '%s' % head_error}
merchantExtendedInfo = {'paymentMode': self.allowed_payment_modes.upper()}
if self.accept_payment == 'auto':
merchantExtendedInfo['autoAccept'] = 'True'
body['merchantExtendedInfo'] = merchantExtendedInfo
payload = {'head': head, 'body': body}
response = self._paytm_make_request('payment/request', payload=payload)
result_code = response.get('resultInfo', {}).get('resultCode')
if result_code == 'A':
return response['resultInfo']
elif result_code == 'F':
return {'error': "%s" % response['resultInfo'].get('resultMsg', _('paytm transaction request declined'))}
default_error_msg = _('makePaymentRequest expected resultCode not found in the response')
error = response.get('error') or default_error_msg
return {'error': '%s' % error}
def paytm_fetch_payment_status(self, transaction_id, reference_id, timestamp):
body = self._paytm_get_request_body(transaction_id, reference_id, timestamp)
head = self._paytm_get_request_head(body)
head_error = head.get('error') and head
if head_error:
return head_error
payload = {'head': head, 'body': body}
response = self._paytm_make_request('V2/payment/status', payload=payload)
result_code = response.get('resultInfo', {}).get('resultCode')
if result_code == 'S':
# Since we don't want to send extra data on RPC call
# Only sending essential data when the transaction is successful
data = response['resultInfo']
data.update({
'authCode': response.get('authCode'),
'issuerMaskCardNo': response.get('issuerMaskCardNo'),
'issuingBankName': response.get('issuingBankName'),
'payMethod': response.get('payMethod'),
'cardType': response.get('cardType'),
'cardScheme': response.get('cardScheme'),
'merchantReferenceNo': response.get('merchantReferenceNo'),
'merchantTransactionId': response.get('merchantTransactionId'),
'transactionDateTime': response.get('transactionDateTime'),
})
return data
elif result_code == 'F':
return {'error': "%s" % response['resultInfo'].get('resultMsg', _('paytm transaction failure'))}
elif result_code == 'P':
return response['resultInfo']
default_error_msg = _('paymentFetchRequest expected resultCode not found in the response')
error = response.get('error') or default_error_msg
return {'error': '%s' % error}
def _paytm_generate_signature(self, params_dict, key):
params_list = []
for k in sorted(params_dict.keys()):
value = params_dict[k]
if value is None or params_dict[k].lower() == "null":
value = ""
params_list.append(str(value))
salt = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(4))
params_list.append(salt)
params_with_salt = '|'.join(params_list)
hashed_params = hashlib.sha256(params_with_salt.encode())
hashed_params_with_salt = hashed_params.hexdigest() + salt
padding = 12 #the padding value is a constant
padded_hashed_params_with_salt = bytes(hashed_params_with_salt + padding * chr(padding), 'utf-8')
try:
cipher = Cipher(algorithms.AES(key.encode()), modes.CBC(iv))
encryptor = cipher.encryptor()
encrypted_hashed_params = encryptor.update(padded_hashed_params_with_salt) + encryptor.finalize()
return base64.b64encode(encrypted_hashed_params).decode("UTF-8")
except ValueError as error:
_logger.warning("Cannot generate PayTM signature. Error: %s", error)
return {'error': '%s' % error}
def _paytm_get_request_body(self, transaction_id, reference_id, timestamp):
time = datetime.fromtimestamp(timestamp).astimezone(tz=tz.gettz('Asia/Kolkata')).strftime("%Y-%m-%d %H:%M:%S")
return {
'paytmMid': self.paytm_mid,
'paytmTid': self.paytm_tid,
'transactionDateTime': time,
'merchantTransactionId': transaction_id,
'merchantReferenceNo': reference_id,
}
def _paytm_get_request_head(self, body):
paytm_signature = self._paytm_generate_signature(body, self.paytm_merchant_key)
error = isinstance(paytm_signature, dict) and paytm_signature.get('error')
if error:
return {'error': '%s' % error}
return {
'requestTimeStamp' : body["transactionDateTime"],
'channelId' : self.channel_id,
'checksum' : paytm_signature,
}
@api.constrains('use_payment_terminal')
def _check_paytm_terminal(self):
for record in self:
if record.use_payment_terminal == 'paytm' and record.company_id.currency_id.name != 'INR':
raise UserError(_('This Payment Terminal is only valid for INR Currency'))
@@ -0,0 +1,19 @@
/** @odoo-module */
import { PaymentScreen } from "@point_of_sale/app/screens/payment_screen/payment_screen";
import { patch } from "@web/core/utils/patch";
import { onMounted } from "@odoo/owl";
patch(PaymentScreen.prototype, {
setup() {
super.setup(...arguments);
onMounted(() => {
const pendingPaymentLine = this.currentOrder.paymentlines.find(
paymentLine => paymentLine.payment_method.use_payment_terminal === 'paytm' && (!paymentLine.is_done() && paymentLine.get_payment_status() !== 'pending')
);
if (pendingPaymentLine) {
pendingPaymentLine.set_payment_status('force_done');
}
});
}
});
+5
View File
@@ -0,0 +1,5 @@
/** @odoo-module */
import { register_payment_method } from "@point_of_sale/app/store/pos_store";
import { PaymentPaytm } from '@pos_paytm/js/payment_paytm';
register_payment_method('paytm', PaymentPaytm);
@@ -0,0 +1,160 @@
/** @odoo-module */
import { PaymentInterface } from "@point_of_sale/app/payment/payment_interface";
import { ErrorPopup } from "@point_of_sale/app/errors/popups/error_popup";
const REQUEST_TIMEOUT = 5000
export class PaymentPaytm extends PaymentInterface {
/**
* @Override
* @param { string } cid
* @returns Promise
*/
async send_payment_request(cid) {
await super.send_payment_request(...arguments);
const paymentLine = this.pos.get_order()?.selected_paymentline;
const order = this.pos?.selectedOrder;
let retry = this._retryCountUtility(order.uid)
let transactionId = order.name.replace(' ', '').replaceAll('-', '').toUpperCase();
if (retry > 0) {
transactionId = transactionId.concat('retry', retry);
}
const transactionAmount = paymentLine.amount*100;
const timeStamp = Math.floor(Date.now() / 1000);
// Preparing Unique Random Reference Id
const referencePrefix = this.pos.config?.name.replace(/\s/g, "").slice(0,4)
const referenceId = referencePrefix.concat(Math.floor(Math.random()*1000000000))
const response = await this.makePaymentRequest(transactionAmount, transactionId, referenceId, timeStamp);
if (!response) {
paymentLine.set_payment_status('force_done');
this._incrementRetry(order.uid);
return false
}
paymentLine.set_payment_status('waitingCard');
const pollResponse = await this.pollPayment(transactionId, referenceId, timeStamp);
if (pollResponse) {
let retry_remove = true
this._retryCountUtility(order.uid, retry_remove)
return true;
}
else {
this._incrementRetry(order.uid);
return false;
}
}
/**
* @Override
* @param {} order
* @param { string } cid
* @returns Promise
*/
async send_payment_cancel(order, cid) {
await super.send_payment_cancel(...arguments);
const paymentLine = this.pos.get_order()?.selected_paymentline;
paymentLine.set_payment_status('retry');
this._incrementRetry(order.uid);
clearTimeout(this.pollTimeout);
return true;
}
/**
* @param { string } transactionId
* @param { string } referenceId
* @param { datetime } timestamp
* @returns Promise
*/
async pollPayment(transactionId, referenceId, timestamp) {
const fetchPaymentStatus = async (resolve, reject) => {
const paymentLine = this.pos.get_order()?.selected_paymentline;
if (!paymentLine || paymentLine.payment_status == 'retry') {
return false;
}
try {
const data = await this.env.services.orm.silent.call(
'pos.payment.method',
'paytm_fetch_payment_status',
[[this.payment_method.id], transactionId, referenceId, timestamp],
);
if (data?.error) {
throw data?.error;
}
let resultCode = data?.resultCode;
if (resultCode === 'S' && data?.merchantReferenceNo != referenceId){
throw this.env._t("Reference number mismatched");
} else if (resultCode === 'S') {
paymentLine.paytm_authcode = data?.authCode;
paymentLine.paytm_issuer_card_no = data?.issuerMaskCardNo;
paymentLine.paytm_issuer_bank = data?.issuingBankName;
paymentLine.paytm_payment_method = data?.payMethod;
paymentLine.card_type = data?.cardType;
paymentLine.paytm_card_scheme = data?.cardScheme;
paymentLine.paytm_reference_no = data?.merchantReferenceNo;
paymentLine.transactionId = data?.merchantTransactionId;
paymentLine.payment_date = data?.transactionDateTime;
return resolve(data);
} else {
this.pollTimeout = setTimeout(fetchPaymentStatus, REQUEST_TIMEOUT, resolve, reject);
}
} catch (error) {
const order = this.pos.selectedOrder;
this._incrementRetry(order.uid);
paymentLine.set_payment_status('force_done');
this._showError(error, 'paytmFetchPaymentStatus');
return resolve(false)
};
};
return new Promise(fetchPaymentStatus);
}
/**
* @param { float } amount
* @param { string } transactionId
* @param { string } referenceId
* @param { datetime } timestamp
* @returns Promise
*/
async makePaymentRequest(amount, transactionId, referenceId, timestamp) {
try {
const data = await this.env.services.orm.silent.call(
'pos.payment.method',
'paytm_make_payment_request',
[[this.payment_method.id], amount, transactionId, referenceId, timestamp],
);
if (data?.error) {
throw data.error;
}
return data;
} catch (error) {
this._showError(error, 'paytmMakePaymentRequest');
return false;
};
}
// ---------------------------------------------------------------------------
// Private methods
// ---------------------------------------------------------------------------
_retryCountUtility(uid, remove=false) {
if(remove){
localStorage.removeItem(uid);
}
else{
return localStorage.getItem(uid) || (localStorage.setItem(uid, 0) && 0);
}
}
_incrementRetry(uid) {
let retry = localStorage.getItem(uid);
localStorage.setItem(uid, ++retry);
}
_showError(error_msg, title) {
var self = this;
self.env.services.popup.add(ErrorPopup, {
title: title || self.env._t('PayTM Error'),
body: error_msg,
});
}
}
@@ -0,0 +1,33 @@
/** @odoo-module */
import { Payment } from "@point_of_sale/app/store/models";
import { patch } from "@web/core/utils/patch";
patch(Payment.prototype, {
init_from_JSON(json) {
super.init_from_JSON(...arguments);
if(this.payment_method?.use_payment_terminal === 'paytm'){
this.paytm_authcode = json.paytm_authcode;
this.paytm_issuer_card_no = json.paytm_issuer_card_no;
this.paytm_issuer_bank = json.paytm_issuer_bank;
this.paytm_payment_method = json.paytm_payment_method;
this.paytm_reference_no = json.paytm_reference_no;
this.paytm_card_scheme = json.paytm_card_scheme;
}
},
export_as_JSON() {
const result = super.export_as_JSON(...arguments);
if(result && this.payment_method?.use_payment_terminal === 'paytm'){
return Object.assign(result, {
paytm_authcode: this.paytm_authcode,
paytm_issuer_card_no: this.paytm_issuer_card_no,
paytm_issuer_bank: this.paytm_issuer_bank,
paytm_payment_method: this.paytm_payment_method,
paytm_reference_no: this.paytm_reference_no,
paytm_card_scheme: this.paytm_card_scheme,
});
}
return result
},
});
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="pos_payment_method_view_form_inherit_pos_paytm" model="ir.ui.view">
<field name="name">pos.payment.method.form.inherit.paytm</field>
<field name="model">pos.payment.method</field>
<field name="inherit_id" ref="point_of_sale.pos_payment_method_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='use_payment_terminal']" position="after">
<field name="paytm_mid" invisible="use_payment_terminal != 'paytm'" required="use_payment_terminal == 'paytm'"/>
<field name="paytm_tid" invisible="use_payment_terminal != 'paytm'" required="use_payment_terminal == 'paytm'"/>
<field name="paytm_merchant_key" invisible="use_payment_terminal != 'paytm'" required="use_payment_terminal == 'paytm'"/>
<field name="channel_id" groups="base.group_no_one" invisible="use_payment_terminal != 'paytm'" required="use_payment_terminal == 'paytm'"/>
<field name="paytm_test_mode" invisible="use_payment_terminal != 'paytm'" required="use_payment_terminal == 'paytm'"/>
<field name="accept_payment" invisible="use_payment_terminal != 'paytm'" required="use_payment_terminal == 'paytm'"/>
<field name="allowed_payment_modes" invisible="use_payment_terminal != 'paytm'" required="use_payment_terminal == 'paytm'"/>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_pos_payment_form" model="ir.ui.view">
<field name="name">pos.payment.form</field>
<field name="model">pos.payment</field>
<field name="inherit_id" ref="point_of_sale.view_pos_payment_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='transaction_id']" position='after'>
<field name="paytm_authcode" invisible="not paytm_authcode"/>
<field name="paytm_issuer_card_no" invisible="not paytm_issuer_card_no"/>
<field name="paytm_issuer_bank" invisible="not paytm_issuer_bank"/>
<field name="paytm_payment_method" invisible="not paytm_payment_method"/>
<field name="paytm_reference_no" invisible="not paytm_reference_no"/>
<field name="paytm_card_scheme" invisible="not paytm_card_scheme"/>
</xpath>
</field>
</record>
</odoo>