Show tax rate in receipt, adjust keypad display (#6739)

* Show tax rate in receipt, adjust keypad display

* Adjust small nits in the view
This commit is contained in:
Nicolas Dorier
2025-05-20 13:31:29 +09:00
committed by GitHub
parent c4097b5ddd
commit 5f7a686833
9 changed files with 68 additions and 23 deletions

View File

@@ -522,23 +522,23 @@ donation:
Assert.Equal("1.234,00", await s.Page.TextContentAsync("#Amount"));
Assert.Equal("", await s.Page.TextContentAsync("#Calculation"));
await EnterKeypad(s, "+56");
Assert.Equal("1.234,56", await s.Page.TextContentAsync("#Amount"));
Assert.Equal("0,56", await s.Page.TextContentAsync("#Amount"));
Assert.True(await s.Page.IsEnabledAsync("#ModeTablist-discount"));
Assert.True(await s.Page.IsEnabledAsync("#ModeTablist-tip"));
await AssertKeypadCalculation(s, "1.234,00 € + 0,56 €");
await AssertKeypadCalculation(s, "1.234,00 € + 0,56 € = 1.234,56 €");
// Discount: 10%
await s.Page.ClickAsync("label[for='ModeTablist-discount']");
await EnterKeypad(s, "10");
Assert.Contains("1.111,10", await s.Page.TextContentAsync("#Amount"));
Assert.Contains("0,56", await s.Page.TextContentAsync("#Amount"));
Assert.Contains("10% discount", await s.Page.TextContentAsync("#Discount"));
await AssertKeypadCalculation(s, "1.234,00 € + 0,56 € - 123,46 € (10%)");
await AssertKeypadCalculation(s, "1.234,00 € + 0,56 € - 123,46 € (10%) = 1.111,10 €");
// Tip: 10%
await s.Page.ClickAsync("label[for='ModeTablist-tip']");
await s.Page.ClickAsync("#Tip-10");
Assert.Contains("1.222,21", await s.Page.TextContentAsync("#Amount"));
await AssertKeypadCalculation(s, "1.234,00 € + 0,56 € - 123,46 € (10%) + 111,11 € (10%)");
Assert.Contains("0,56", await s.Page.TextContentAsync("#Amount"));
await AssertKeypadCalculation(s, "1.234,00 € + 0,56 € - 123,46 € (10%) + 111,11 € (10%) = 1.222,21 €");
// Pay
await s.Page.ClickAsync("#pay-button");
@@ -580,8 +580,8 @@ donation:
await s.Page.ClickAsync("#ItemsListOffcanvas button[data-bs-dismiss='offcanvas']");
await EnterKeypad(s, "123");
Assert.Contains("4,65", await s.Page.TextContentAsync("#Amount"));
await AssertKeypadCalculation(s, "2 x Green Tea (1,00 €) = 2,00 € + 1 x Black Tea (1,00 €) = 1,00 € + 1,23 € + 0,42 € (10%)");
Assert.Contains("1,23", await s.Page.TextContentAsync("#Amount"));
await AssertKeypadCalculation(s, "2 x Green Tea (1,00 €) = 2,00 € + 1 x Black Tea (1,00 €) = 1,00 € + 1,23 € + 0,42 € (10%) = 4,65 €");
// Pay
await s.Page.ClickAsync("#pay-button");
@@ -604,7 +604,7 @@ donation:
],
Sums = [
new("Subtotal", "4,23 €"),
new("Tax", "0,42 €"),
new("Tax", "0,42 € (10%)"),
new("Total", "4,65 €")
]
});

View File

@@ -345,6 +345,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
if (jposData.Tax > 0)
{
var taxFormatted = _displayFormatter.Currency(jposData.Tax, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol);
if (order.GetTaxRate() is { } r)
taxFormatted = $"{taxFormatted} ({r:0.######}%)";
receiptData.Tax = taxFormatted;
}

View File

@@ -1,6 +1,7 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
namespace BTCPayServer.Plugins.PointOfSale;
@@ -64,6 +65,20 @@ public class PoSOrder
_tip = Round(tip);
}
/// <summary>
/// Returns the tax rate of the items in the cart.
/// If the tax rates are not all the same, returns null.
/// If the cart is empty, returns null.
/// Else, returns the tax rate shared by all items
/// </summary>
/// <returns></returns>
public decimal? GetTaxRate()
{
if (!ItemLines.Any())
return null;
return ItemLines.GroupBy(i => i.TaxRate).Count() == 1 ? ItemLines[0].TaxRate : null;
}
/// <summary>
///
/// </summary>

View File

@@ -191,9 +191,11 @@
<tr v-if="showDiscount">
<th class="align-middle" text-translate="true">Discount</th>
<th class="align-middle" colspan="3">
<div class="input-group input-group-sm w-100px pull-right">
<input class="form-control hide-number-spin" type="number" inputmode="decimal" min="0" step="1" max="100" id="Discount" v-model.number="discountPercent">
<span class="input-group-text">%</span>
<div class="d-flex justify-content-end">
<div class="input-group input-group-sm w-100px">
<input class="form-control hide-number-spin" type="number" inputmode="decimal" min="0" step="1" max="100" id="Discount" v-model.number="discountPercent">
<span class="input-group-text">%</span>
</div>
</div>
</th>
</tr>
@@ -233,7 +235,7 @@
<tr v-if="discountNumeric">
<td class="align-middle" text-translate="true">Discount</td>
<td class="align-middle text-end" id="CartDiscount">
<span>{{ formatCurrency(discountNumeric, true) }}</span>&nbsp;<span v-if="discountPercent">({{discountPercent}}%)</span>
<span>{{ formatCurrency(discountNumeric, true) }}</span> <span v-if="discountPercent">({{discountPercent}}%)</span>
</td>
</tr>
<tr v-if="subtotalNumeric">
@@ -243,13 +245,13 @@
<tr v-if="tipNumeric">
<td class="align-middle" text-translate="true">Tip</td>
<td class="align-middle text-end" id="CartTip">
<span>{{ formatCurrency(tipNumeric, true) }}</span>&nbsp;<span v-if="tipPercent">({{tipPercent}}%)</span>
<span>{{ formatCurrency(tipNumeric, true) }}</span> <span v-if="tipPercent">({{tipPercent}}%)</span>
</td>
</tr>
<tr v-if="taxNumeric">
<td class="align-middle" text-translate="true">Taxes</td>
<td class="align-middle text-end" id="CartTax">
{{ formatCurrency(taxNumeric, true) }}
<span>{{ formatCurrency(taxNumeric, true) }}</span> <span v-if="taxPercent">({{taxPercent}}%)</span>
</td>
</tr>
<tr>

View File

@@ -22,7 +22,7 @@
<input type="hidden" name="posdata" :value="posdata">
<div ref="display" class="d-flex flex-column align-items-center px-4 mb-auto">
<div class="fw-semibold text-muted" id="Currency">{{currencyCode}}</div>
<div class="fw-bold lh-sm" ref="amount" v-bind:style="{ fontSize: `${fontSize}px` }" id="Amount">{{ formatCurrency(totalNumeric, false) }}</div>
<div class="fw-bold lh-sm" ref="amount" v-bind:style="{ fontSize: `${fontSize}px` }" id="Amount">{{ formatCurrency(lastAmount, false) }}</div>
<div class="text-muted text-center mt-2" id="Calculation">{{ calculation }}</div>
</div>
<div id="ModeTabs" class="tab-content mb-n2" v-if="showDiscount || enableTips">

View File

@@ -34,7 +34,7 @@
<tfoot style="border-top-width:0">
@if (posData.ItemsTotal != null)
{
<tr style="border-top-width:3px">
<tr>
<th text-translate="true">Items total</th>
<td class="text-end">@posData.ItemsTotal</td>
</tr>
@@ -48,7 +48,7 @@
}
@if (posData.Subtotal != null)
{
<tr style="border-top-width:3px">
<tr>
<th text-translate="true">Subtotal</th>
<td class="text-end">@posData.Subtotal</td>
</tr>

View File

@@ -139,9 +139,6 @@
@if (posData.ItemsTotal != null)
{
<tr>
<td colspan="2"><hr class="w-100 my-0"/></td>
</tr>
<tr class="sums-data">
<td class="key text-secondary">Items total</td>
<td class="val text-end">@posData.ItemsTotal</td>

View File

@@ -50,6 +50,21 @@ class PoSOrder {
}
}
// Returns the tax rate of the items in the cart.
// If the tax rates are not all the same, returns null.
// If the cart is empty, returns null.
// Else, returns the tax rate shared by all items
getTaxRate() {
if (this.itemLines.length === 0) return null;
var rate = this.itemLines[0].taxRate ?? 0;
for (const line of this.itemLines.slice(1)) {
if (rate !== line.taxRate)
{
return null;
}
}
return rate;
}
calculate() {
const ctx = {
discount: 0,
@@ -159,6 +174,9 @@ const posCommon = {
taxNumeric() {
return this.summary.tax;
},
taxPercent() {
return this.posOrder.getTaxRate();
},
subtotalNumeric () {
// We don't want to show the subtotal if there is no tax or tips
if (this.summary.priceTaxExcluded === this.summary.priceTaxIncludedWithTips) return 0;
@@ -185,6 +203,9 @@ const posCommon = {
tipNumeric () {
return this.summary.tip;
},
lastAmount() {
return this.amounts[this.amounts.length - 1] = this.amounts[this.amounts.length - 1] || 0;
},
totalNumeric () {
return this.summary.priceTaxIncludedWithTips;
},

View File

@@ -1,4 +1,5 @@
document.addEventListener("DOMContentLoaded",function () {
const displayFontSize = 64;
new Vue({
el: '#app',
@@ -39,8 +40,15 @@ document.addEventListener("DOMContentLoaded",function () {
if (this.discountNumeric > 0 || this.discountPercentNumeric > 0) calc += ` - ${this.formatCurrency(this.discountNumeric, true)} (${this.discountPercent}%)`
if (this.summary.tip > 0) calc += ` + ${this.formatCurrency(this.summary.tip, true)}`
if (this.tipPercent) calc += ` (${this.tipPercent}%)`
if (this.summary.tax) calc += ` + ${this.formatCurrency(this.summary.tax, true)}`
if (this.defaultTaxRate) calc += ` (${this.defaultTaxRate}%)`
if (this.summary.tax)
{
calc += ` + ${this.formatCurrency(this.summary.tax, true)}`
if (this.posOrder.getTaxRate())
{
calc += ` (${this.posOrder.getTaxRate()}%)`
}
}
calc += ` = ${this.formatCurrency(this.summary.priceTaxIncludedWithTips, true)}`
return calc
}
},