Generate QR codes and print them on receipts for India
This article provides customization guidelines and explains how to generate Unified Payments Interface (UPI) Quick Response (QR) codes and print them on receipts for India.
Prerequisites
Functionality for generating QR codes in the Commerce runtime (CRT) was introduced in Microsoft Dynamics 365 Commerce version 10.0.13. Therefore, the information in this article is valid only for version 10.0.13 and later.
Commerce versions 10.0.17 and later support printing QR codes on receipts using Retail Hardware Station. In versions 10.0.16 and earlier, QR codes can only be printed from Modern POS (MPOS).
Data schema changes
Because invoice printing isn't supported in Dynamics 365 Commerce, there are no UPI-related fields in Point of sale (POS). Therefore, the fields should be added in the scope of code customization. We recommend that you extend the RetailStoreTenderTypeTable table by adding two string fields: UPIId and UPIName. As part of this customization, you should extend the RetailStoreTenderTypeTable form to edit the fields in the user interface (UI). You should also extend the channel data schema and download jobs. For more information, see Channel database extensions.
Set up receipts in Commerce headquarters
Create a new language text that will be used for a new custom receipt field for a QR code:
Go to Retail and Commerce > Retail and commerce IT > Channel setup > POS profiles > Language text.
On the POS tab, on the POS language text FastTab, select Add to create a language text.
In the Language ID field, specify the language of the new language text (for example, en-us for US English).
In the Text ID field, enter the identifier of the new language text.
In the Text field, enter the new language text (for example, Tax invoice QR code).
Create a new custom receipt field for a QR code:
Go to Retail and Commerce > Retail and commerce IT > Channel setup > POS profiles > Custom field.
On the Action Pane, select New to add a field.
In the Name field, enter a name for the new field (for example, TAXINVOICE_QR).
In the Type field, select Receipt.
In the Caption text ID field, enter the Text ID value from the language text that you created earlier.
Add a custom field for a QR code to a receipt:
- Go to Retail and Commerce > Retail and commerce IT > Channel setup > POS > Receipt formats.
- Select the receipt to add a QR code to.
- On the Action Pane, select Designer.
- Download the receipt designer, run it, and sign in.
- Add a custom field to the receipt header or footer.
Sync the changes with the channel database:
- Go to Retail and Commerce > Retail and commerce IT > Distribution schedule.
- Run jobs 1070 and 1090.
Create a CRT extension to support printing QR codes
To support the new custom receipt field for a QR code, you must create a CRT extension that will create a URL string and generate a QR code for it. For an example that shows how to add a custom field to a receipt, see Extend Commerce Store receipts.
Follow these steps to handle the new custom receipt field for a QR code.
- Install the Retail software development kit (SDK). For more information, see Retail software development kit (SDK).
- In the Retail SDK, create a C# project.
- In the new C# project, add references to the following packages per Commerce release:
- Microsoft.Dynamics.Commerce.Runtime.DataModel.India
- Microsoft.Dynamics.Commerce.Runtime.DataServices
- Microsoft.Dynamics.Commerce.Runtime.ElectronicReporting
- Microsoft.Dynamics.Commerce.Runtime.GenericTaxEngine
- Microsoft.Dynamics.Commerce.Runtime.Services.Messages
Create a class to handle.
public class GetSalesTransactionCustomReceiptFieldService : IRequestHandlerAsync, ICountryRegionAware. { // Add code here. }
Implement a handler method that will handle the new custom receipt field and return a QR code as a string.
/// <summary> /// Gets the custom receipt field value for sales receipt. /// </summary> /// <param name="request>The service request to get custom receipt field value.</param> /// <returns>The value of custom receipt field.</returns> private async Task<Response> GetCustomReceiptFieldForSalesTransactionReceiptsAsync(GetSalesTransactionCustomReceiptFieldServiceRequest request) { ThrowIf.Null(request.SalesOrder, "sales order"); string receiptFieldName = request.CustomReceiptField; string receiptFieldValue = string.Empty; switch (receiptFieldName) { case "TAXINVOICE_QR": receiptFieldValue = await GetQRCode(request).ConfigureAwait(false); break; default: return new NotHandledResponse(); } return new GetCustomReceiptFieldServiceResponse(receiptFieldValue); }
The handler should include the following steps.
Generate UPI link based on the sales order (SalesOrder) that is passed as the request parameter.
Run EncodeQrCodeServiceRequest to generate a QR code. (EncodeQrCodeServiceRequest is part of the Microsoft.Dynamics.Commerce.Runtime.ElectronicReporting package.)
Wrap the QR code string that is returned in a
<>
tag.var qrCodeRequest = new EncodeQrCodeServiceRequest(stringBuilder.ToString()) { Width = 150, // Replace with desired QR code width Height = 150 // Replace with desired QR code width }; EncodeQrCodeServiceResponse qrCodeDataResponse = await request.RequestContext.ExecuteAsync<EncodeQrCodeServiceResponse>(qrCodeRequest).ConfigureAwait(false); receiptFieldValue = $"<I:{qrCodeDataResponse.QRcode}>"; return receiptFieldValue;
Add the required extensions to CommerceRuntime.Ext.config. Here, Contoso.Commerce.Runtime.ReceiptsIndia is the name of the new extension for printing the QR code assembly.
<commerceRuntimeExtensions> <composition> <!-- Register your own assemblies or types here. The following example registers MyNewCrtService (and all its request handlers). Any other services are not being overridden: <add source="type" value="Contoso.Commerce.Runtime.MyNewCrtService, Contoso.Commerce.Runtime.Services" /> >add source="assembly" value="Contoso.Commerce.Runtime.Services" /> --> <add source="assembly" value="Contoso.Commerce.Runtime.ReceiptsIndia" /> <add source="assembly" value="Microsoft.Dynamics.Commerce.Runtime.ElectronicReporting" /> <add source="assembly" value="Microsoft.Dynamics.Commerce.Runtime.GenericTaxEngine" /> </composition> </commerceRuntimeExtensions>
Printing QR code images on OPOS printers
When using an OPOS printer, you may need to convert the QR code image from the png format to the bmp format. Below is an example of this conversion.
...
var qrCodeRequest = new
EncodeQrCodeServiceRequest(stringBuilder.ToString())
{
Width = 150, // Replace with desired QR code width
Height = 150 // Replace with desired QR code width
};
EncodeQrCodeServiceResponse qrCodeDataResponse = await
request.RequestContext.ExecuteAsync<EncodeQrCodeServiceResponse>(qrCodeRequest).ConfigureAwait(false);
string qrCode = ConvertImagePNGToBMP(qrCodeDataResponse.QRcode);
receiptFieldValue = $"<I:{qrCode}>";
return receiptFieldValue;
...
/// <summary>
/// Converts an image from "png" to "bmp".
/// </summary>
/// <param name="qrCode">Base64 represents the image to convert.</param>
/// <returns>The image as base64.</returns>
private static string ConvertImagePNGToBMP(string qrCode)
{
string convertedQRCode = qrCode;
byte[] imageBytes = Convert.FromBase64String(qrCode);
using (MemoryStream msFrom = new MemoryStream(imageBytes))
{
var image = Image.FromStream(msFrom);
using (MemoryStream msTo = new MemoryStream())
{
image.Save(msTo, ImageFormat.Bmp);
convertedQRCode = Convert.ToBase64String(msTo.ToArray());
}
}
return convertedQRCode;
}
Samples of a CRT extension class for printing QR codes
The following code blocks grouped by Commerce release provide samples of CRT extension classes for printing QR codes.
namespace Contoso
{
namespace Commerce.Runtime.ReceiptsIndia
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Dynamics.Commerce.Runtime;
using Microsoft.Dynamics.Commerce.Runtime.DataModel;
using Microsoft.Dynamics.Commerce.Runtime.DataServices.Messages;
using Microsoft.Dynamics.Commerce.Runtime.Messages;
using Microsoft.Dynamics.Commerce.Runtime.Services.Messages;
/// <summary>
/// The extended service to get custom sales receipt field.
/// </summary>
public class GetSalesTransactionCustomReceiptFieldService : IRequestHandlerAsync, ICountryRegionAware
{
/// <summary>
/// Gets the supported request types.
/// </summary>
public IEnumerable<Type> SupportedRequestTypes
{
get
{
return new[]
{
typeof(GetSalesTransactionCustomReceiptFieldServiceRequest),
};
}
}
/// <summary>
/// Gets a collection of companies supported by this request handler.
/// </summary>
public IEnumerable<string> SupportedCountryRegions
{
get
{
return new[]
{
nameof(CountryRegionISOCode.IN),
};
}
}
/// <summary>
/// Executes the requests.
/// </summary>
/// <param name="request">The request parameter.</param>
/// <returns>The GetReceiptServiceResponse that contains the formatted receipts.</returns>
public async Task<Response> Execute(Request request)
{
ThrowIf.Null(request, nameof(request));
Type requestedType = request.GetType();
if (requestedType == typeof(GetSalesTransactionCustomReceiptFieldServiceRequest))
{
return await
this.GetCustomReceiptFieldForSalesTransactionReceiptsAsync((GetSalesTransactionCustomReceiptFieldServiceRequest)request).ConfigureAwait(false);
}
throw new NotSupportedException(string.Format("Request '{0}' is not supported.", request.GetType()));
}
/// <summary>
/// Gets the custom receipt field value for sales receipt.
/// </summary>
/// <param name="request">The service request to get custom receipt field value.</param>
/// <returns>The value of custom receipt field.<returns>
private async Task<Response>
GetCustomReceiptFieldForSalesTransactionReceiptsAsync(GetSalesTransactionCustomReceiptFieldServiceRequest request)
{
ThrowIf.Null(request.SalesOrder,
$"{nameof(request)},{nameof(request.SalesOrder)}");
string receiptFieldName = request.CustomReceiptField;
string receiptFieldValue = string.Empty;
switch (receiptFieldName)
{
case "TAXINVOICE_QR":
receiptFieldValue = await
GetQRCode(request).ConfigureAwait(false);
break;
default:
return new NotHandledResponse();
}
return new GetCustomReceiptFieldServiceResponse(receiptFieldValue);
}
/// <summary>
/// Gets the QR code for the receipt.
/// </summary>
/// <param name="request">The service request to get customreceipt field value.</param>
/// <returns>QR code custom field value.</returns>
private static async Task<string>
GetQRCode(GetSalesTransactionCustomReceiptFieldServiceRequest request)
{
var salesOrder = request.SalesOrder;
string receiptFieldValue = string.Empty;
bool isB2C = await IsB2CTransactionAsync(request.RequestContext, salesOrder).ConfigureAwait(false);
if (isB2C)
{
string gstNumber = await GetStoreGSTIN(request.RequestContext).ConfigureAwait(false);
var paymentInfo = await GetPaymentUPIInfo(request.RequestContext, salesOrder).ConfigureAwait(false);
string totalAmount = FormatAmount(salesOrder.TotalAmount);
string igstAmt = FormatAmount(GetTaxComponentAmount(salesOrder,
"IGST"));
string cgstAmt = FormatAmount(GetTaxComponentAmount(salesOrder,
"CGST"));
string sgstAmt = FormatAmount(GetTaxComponentAmount(salesOrder,
"SGST"));
string cessAmt = FormatAmount(GetTaxComponentAmount(salesOrder,
"CESS"));
StringBuilder stringBuilder = new
StringBuilder($"upi://pay?cu={salesOrder.CurrencyCode}");
stringBuilder.Append($"&am={totalAmount}");
stringBuilder.Append($"&pa={paymentInfo.Item1}");
stringBuilder.Append($"&pn={paymentInfo.Item2}");
stringBuilder.Append($"&tr={salesOrder.ReceiptId}");
stringBuilder.Append($"&dt={salesOrder.TransactionDateTime.ToString("dd/MM/yyyy")}");
stringBuilder.Append($"&no={gstNumber}");
stringBuilder.Append($"&IgstAmt = {igstAmt}");
stringBuilder.Append($"&CgstAmt = {cgstAmt}");
stringBuilder.Append($"&SgstAmt = {sgstAmt}");
stringBuilder.Append($"&CesAmt = {cessAmt}");
var qrCodeRequest = new
EncodeQrCodeServiceRequest(stringBuilder.ToString())
{
Width = 150, // Replace with desired QR code width
Height = 150 // Replace with desired QR code width
};
EncodeQrCodeServiceResponse qrCodeDataResponse = await
request.RequestContext.ExecuteAsync<EncodeQrCodeServiceResponse>(qrCodeRequest).ConfigureAwait(false);
receiptFieldValue = $"<I:{qrCodeDataResponse.QRcode}>";
}
return receiptFieldValue;
}
/// <summary>
/// Gets store GSTIN.
/// </summary>
/// <param name="requestContext">Request context.</param>
/// <returns>Store GSTIN.</returns>
private static async Task<string> GetStoreGSTIN(RequestContext
requestContext)
{
var dataRequest = new GetReceiptHeaderGteTaxInfoIndiaDataRequest
{
QueryResultSettings = QueryResultSettings.SingleRecord,
};
var headerTaxInformation = await
requestContext.ExecuteAsync<SingleEntityDataServiceResponse<ReceiptHeaderGteTaxInfoIndia>>(dataRequest).ConfigureAwait(false);
return headerTaxInformation.Entity?.GstRegistrationNumber;
}
/// <summary>
/// Gets payment UPI info for the sales order.
/// </summary>
/// <param name="requestContext">Request context.</param>
/// <param name="salesOrder">Sales order.</param>
/// <returns>Payment info.</returns>
private static async Task<Tuple<string, string>>
GetPaymentUPIInfo(RequestContext requestContext, SalesOrder salesOrder)
{
var channelTenderDataRequest = new
GetChannelTenderTypesDataRequest(requestContext.GetPrincipal().ChannelId,
QueryResultSettings.AllRecords);
var channelTenderTypes = (await
requestContext.Runtime.ExecuteAsync<EntityDataServiceResponse<TenderType>>(channelTenderDataRequest,
requestContext).ConfigureAwait(false)).PagedEntityCollection.Results;
string upiId = string.Empty;
string upiName = string.Empty;
int count = salesOrder.ActiveTenderLines.Count;
foreach (var tenderLine in salesOrder.ActiveTenderLines)
{
TenderType tenderType = channelTenderTypes.Where(type =>
type.TenderTypeId == tenderLine.TenderTypeId).SingleOrDefault();
if (tenderType == null)
{
continue;
}
if (!string.IsNullOrEmpty(upiId))
{
upiId += ",";
}
if (!string.IsNullOrEmpty(upiName))
{
upiName += ",";
}
upiId += tenderType.TenderTypeId; // Here should be customized field UPIId
upiName += tenderType.Name; // Here should be customized field UPIName
if (count > 1)
{
string amount = FormatAmount(tenderLine.Amount);
upiId += $":{amount}";
upiName += $":{amount}";
}
}
return new Tuple<string, string>(upiId, upiName);
}
/// <summary>
/// Gets tax component amount.
/// </summary>
/// <param name="salesOrder">Sales order.</param>
/// <param name="taxComponent">Tax component.</param>
/// <returns>Tax component amount.</returns>
private static decimal GetTaxComponentAmount(SalesOrder salesOrder, string taxComponent)
{
decimal taxAmount = 0m;
IEnumerable<TaxLineGTE> taxLineGte =
salesOrder.ActiveSalesLines.SelectMany(x => x.TaxLines).OfType<TaxLineGTE>();
var groups = taxLineGte.GroupBy(x => new { x.TaxComponent }).OrderBy(x =>
x.Key.TaxComponent);
var group = groups.Where(g => string.Equals(g.Key.TaxComponent,
taxComponent, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
if (group != null)
{
taxAmount = group.Sum(l => l.Amount);
}
return taxAmount;
}
/// <summary>
/// Gets the store customer account number based on store type.
/// </summary>
/// <returns>The store customer account number.</returns>
internal static string
GetStoreCustomerAccountNumberFromChannelProperties(RequestContext requestContext)
{
if (requestContext.GetChannelConfiguration().IsOnlineStore())
{
if (requestContext.GetPrincipal().IsCustomer)
{
return requestContext.GetPrincipal().UserId;
}
}
return requestContext.GetChannel().DefaultCustomerAccount;
}
///<summary>
/// Defines whether the customer is store default customer.
/// </summary>
/// <param name="requestContext">Request context.</param>
/// <param name="customerId">Customer id.</param>
/// <returns>rue if the customer is store default customer; false otherwise.</returns>
private static bool IsStoreCustomer(RequestContext requestContext, string customerId)
{
return string.IsNullOrEmpty(customerId)
|| string.Equals(customerId,
GetStoreCustomerAccountNumberFromChannelProperties(requestContext),
StringComparison.InvariantCultureIgnoreCase);
}
/// <summary>
/// Defines whether the transaction is B2C.
/// </summary>
/// <param name="requestContext">Request context.</param>
/// <param name="salesOrder">Sales order.</param>
/// <returns>True if the transaction is B2C; false otherwise.</returns>
private static async Task<bool> IsB2CTransactionAsync(RequestContext requestContext, SalesOrder salesOrder)
{
if (IsStoreCustomer(requestContext, salesOrder.CustomerId))
{
return true;
}
var customer = await GetCustomerAsync(requestContext, salesOrder.CustomerId).ConfigureAwait(false);
if (customer == null)
{
return true;
}
var address = customer.GetPrimaryAddress();
if (address == null)
{
return true;
}
GetPrimaryAddressTaxInformationDataRequest
getPrimaryAddressTaxInformationDataRequest = new
GetPrimaryAddressTaxInformationDataRequest(address.LogisticsLocationRecordId);
AddressTaxInformationIndia addressTaxInformationIndia = (await
requestContext.Runtime.ExecuteAsync
<SingleEntityDataServiceResponse<AddressTaxInformationIndia>>(getPrimaryAddressTaxInformationDataRequest,
requestContext)
.ConfigureAwait(false)).Entity;
if (addressTaxInformationIndia == null ||
addressTaxInformationIndia.GstinRegistrationNumber == null
||
string.IsNullOrEmpty(addressTaxInformationIndia.GstinRegistrationNumber.RegistrationNumber))
{
return true;
}
return false;
}
/// <summary>
/// Gets customer by customer identifier.
/// </summary>
/// <param name="customerId">Customer identifier.</param>
/// <returns>Customer object.</returns>
private static async Task<Customer> GetCustomerAsync(RequestContext requestContext, string customerId)
{
Customer customer = null;
if (!string.IsNullOrWhiteSpace(customerId))
{
var getCustomerDataRequest = new GetCustomerDataRequest(customerId);
SingleEntityDataServiceResponse<Customer> getCustomerDataResponse = await
requestContext.ExecuteAsync<SingleEntityDataServiceResponse<Customer>>(getCustomerDataRequest).ConfigureAwait(false);
customer = getCustomerDataResponse.Entity;
}
return customer;
}
/// <summary>
/// Formats amount.
/// </summary>
/// <param name="amount">Amount to format.</param>
/// <returns>Formatted amount.</returns>
private static string FormatAmount(decimal amount)
{
return amount.ToString("0.00", CultureInfo.InvariantCulture);
}
}
}
}