Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,3 +267,14 @@ async def m018_add_stripe_reader_id(db: Database):
ALTER TABLE tpos.pos ADD stripe_reader_id TEXT NULL;
"""
)


async def m019_add_receipt_sats_only(db: Database):
"""
Add receipt option to only show sats on bitcoin transactions
"""
await db.execute(
"""
ALTER TABLE tpos.pos ADD only_show_sats_on_bitcoin BOOLEAN DEFAULT true;
"""
)
3 changes: 3 additions & 0 deletions models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class CreateTposInvoice(BaseModel):
memo: str | None = Query(None)
exchange_rate: float | None = Query(None, ge=0.0)
details: dict | None = Query(None)
notes: dict | None = Query(None)
inventory: InventorySale | None = Query(None)
tip_amount: int | None = Query(None, ge=1)
user_lnaddress: str | None = Query(None)
Expand Down Expand Up @@ -63,6 +64,7 @@ class CreateTposData(BaseModel):
business_name: str | None
business_address: str | None
business_vat_id: str | None
only_show_sats_on_bitcoin: bool = Query(True)
fiat_provider: str | None = Field(None)
stripe_card_payments: bool = False
stripe_reader_id: str | None = None
Expand Down Expand Up @@ -96,6 +98,7 @@ class TposClean(BaseModel):
business_name: str | None = None
business_address: str | None = None
business_vat_id: str | None = None
only_show_sats_on_bitcoin: bool = True
fiat_provider: str | None = None
stripe_card_payments: bool = False
stripe_reader_id: str | None = None
Expand Down
1 change: 0 additions & 1 deletion services.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from typing import Any

import httpx

from lnbits.core.crud import get_wallet
from lnbits.core.models import User
from lnbits.helpers import create_access_token
Expand Down
53 changes: 53 additions & 0 deletions static/components/order-receipt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
window.app.component('order-receipt', {
name: 'order-receipt',
props: ['data', 'type'],
data() {},
template: `
<div class="q-pa-md">
<div class="text-center q-mb-xl">
<p class='text-subtitle2 text-uppercase'>Order</p>
</div>
<q-table v-if="data.extra.details.items && data.extra.details.items.length > 0"
dense
class="q-ma-none"
:hide-pagination="true"
:rows-per-page-options="[0]"
:rows="data.extra.details.items"
class="q-pa-none text-caption"
:columns="[
{ name: 'title', label: 'Item', field: 'title' },
{ name: 'quantity', label: 'Qty', field: 'quantity' },
]"
row-key="title"
hide-bottom
>
<template v-slot:header="props">
<q-tr :props="props">
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
>
<span class="q-pa-none" v-text="col.label"></span>
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td key="title" :props="props" class="q-pa-none">
<div class="text-subtitle2" v-text="props.row.title"></div>
<div
v-if="props.row.note"
class="text-subtitle2 text-italic"
v-text="props.row.note"
></div>
</q-td>
<q-td key="quantity" :props="props" class="q-pa-none">
<span class="text-subtitle2 text-no-wrap" v-text="props.row.quantity"></span>
</q-td>
</q-tr>
</template>
</q-table>
</div>
`
})
57 changes: 53 additions & 4 deletions static/components/receipt.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ window.app.component('receipt', {
},
currencyText() {
return `(${this.currency})`
},
isBitcoinTransaction() {
return !(
this.data.extra?.paid_in_fiat ||
this.data.extra?.fiat_method ||
this.data.extra?.fiat_payment_request
)
},
showBitcoinDetails() {
const onlyShowOnBitcoin = this.data.only_show_sats_on_bitcoin !== false
return !onlyShowOnBitcoin || this.isBitcoinTransaction
}
},
methods: {
Expand All @@ -66,7 +77,9 @@ window.app.component('receipt', {
<div class="text-center q-mb-xl">
<p class='text-h6 text-uppercase'>Receipt</p>
<p class=''><span v-text="formattedDate"></span></p>
<p class=''><span v-text="exchangeRateInfo"></span></p>
<p class='' v-if="showBitcoinDetails">
<span v-text="exchangeRateInfo"></span>
</p>
</div>
<div v-if=data.business_name>
<span v-text="data.business_name"></span>
Expand All @@ -79,17 +92,53 @@ window.app.component('receipt', {
<span v-text="data.business_vat_id"></span>
</div>
<q-table v-if="data.extra.details.items && data.extra.details.items.length > 0"
dense
class="q-ma-none"
:hide-pagination="true"
:rows-per-page-options="[0]"
:rows="data.extra.details.items"
class="q-pa-none text-caption"
:columns="[
{ name: 'title', label: 'Item', field: 'title' },
{ name: 'formattedPrice', label: 'Unit Price', field: 'formattedPrice', align: 'right' },
{ name: 'quantity', label: 'Quantity', field: 'quantity' },
{ name: 'formattedPrice', label: 'Price', field: 'formattedPrice', align: 'right' },
{ name: 'quantity', label: 'Qty', field: 'quantity' },
]"
row-key="title"
hide-bottom
>
<template v-slot:header="props">
<q-tr :props="props">
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
>
<span class="q-pa-none text-subtitle2 text-no-wrap" v-text="col.label"></span>
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td key="title" :props="props" class="q-pa-none">
<div class="text-subtitle2" v-text="props.row.title"></div>
<div
v-if="props.row.note"
class="text-subtitle2 text-italic"
v-text="props.row.note"
></div>
</q-td>
<q-td
key="formattedPrice"
:props="props"
class="q-pa-none text-right text-no-wrap"
>
<span class="text-subtitle2 text-no-wrap" v-text="props.row.formattedPrice"></span>
</q-td>
<q-td key="quantity" :props="props" class="q-pa-none">
<span class="text-subtitle2 text-no-wrap" v-text="props.row.quantity"></span>
</q-td>
</q-tr>
</template>
</q-table>
<div class="q-my-xl q-gutter-md">
<div class="row">
Expand Down Expand Up @@ -119,7 +168,7 @@ window.app.component('receipt', {
<span v-text="cartTotal.toFixed(2)"></span>
</div>
</div>
<div class="row">
<div class="row" v-if="showBitcoinDetails">
<div class="col-6">Total (sats)</div>
<div class="col-6 text-right">
<span v-text="data.extra.amount"></span>
Expand Down
3 changes: 3 additions & 0 deletions static/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const mapTpos = obj => {
obj.inventory_omit_tags = omitTagString
? omitTagString.split(',').filter(Boolean)
: []
obj.only_show_sats_on_bitcoin = obj.only_show_sats_on_bitcoin ?? true
obj.itemsMap = new Map()
obj.items.forEach((item, idx) => {
let id = `${obj.id}:${idx + 1}`
Expand Down Expand Up @@ -106,6 +107,7 @@ window.app = Vue.createApp({
lnaddress: false,
lnaddress_cut: 2,
enable_receipt_print: false,
only_show_sats_on_bitcoin: true,
fiat: false,
stripe_card_payments: false,
stripe_reader_id: ''
Expand Down Expand Up @@ -241,6 +243,7 @@ window.app = Vue.createApp({
lnaddress: false,
lnaddress_cut: 2,
enable_receipt_print: false,
only_show_sats_on_bitcoin: true,
fiat: false,
stripe_card_payments: false,
stripe_reader_id: ''
Expand Down
103 changes: 89 additions & 14 deletions static/js/tpos.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ window.app = Vue.createApp({
addedAmount: 0,
enablePrint: false,
receiptData: null,
orderReceipt: false,
printDialog: {
show: false,
paymentHash: null
},
paymentDetails: null,
currency_choice: false,
_currencyResolver: null,
Expand Down Expand Up @@ -304,7 +309,8 @@ window.app = Vue.createApp({
} else {
this.cart.set(item.id, {
...item,
quantity: quantity
quantity: quantity,
note: item.note || null
})
}
this.total = this.total + this.calculateItemPrice(priceSource, quantity)
Expand Down Expand Up @@ -353,6 +359,25 @@ window.app = Vue.createApp({
this.updateCartItemPrice(cartItem, newPrice)
})
},
promptItemNote(item) {
const cartItem = this.cart.get(item.id)
if (!cartItem) return
this.$q
.dialog({
title: 'Set note',
message: 'Add a note for this item',
prompt: {
model: cartItem.note || '',
type: 'text',
placeholder: 'e.g. allergy info'
},
cancel: true
})
.onOk(val => {
const note = (val || '').trim()
this.updateCartItemNote(cartItem, note || null)
})
},
updateCartItemPrice(cartItem, newPrice) {
const roundedPrice =
this.currency === 'sats' ? Math.ceil(newPrice) : +newPrice.toFixed(2)
Expand All @@ -372,6 +397,15 @@ window.app = Vue.createApp({
this.total = +(this.total - oldItemTotal + newItemTotal).toFixed(2)
this.cartTaxTotal()
},
updateCartItemNote(cartItem, note) {
const existing = this.cart.get(cartItem.id)
if (!existing) return
const updatedItem = {
...existing,
note: note
}
this.cart.set(cartItem.id, updatedItem)
},
calculateItemPrice(item, qty) {
let tax = item.tax || this.taxDefault
if (!tax || this.taxInclusive) return item.price * qty
Expand Down Expand Up @@ -751,11 +785,21 @@ window.app = Vue.createApp({
formattedPrice: item.formattedPrice,
quantity: item.quantity,
title: item.title,
tax: item.tax || this.taxDefault
tax: item.tax || this.taxDefault,
note: item.note || null
})),
taxIncluded: this.taxInclusive,
taxValue: this.cartTax
}
const notes = {}
;[...this.cart.values()].forEach(item => {
if (item.note) {
notes[item.title] = item.note
}
})
if (Object.keys(notes).length) {
params.notes = notes
}
}
if (this.lnaddress) {
params.user_lnaddress = this.lnaddressDialog.lnaddress
Expand Down Expand Up @@ -837,7 +881,7 @@ window.app = Vue.createApp({
this.clearCart()
this.showComplete()
if (this.enablePrint) {
this.printReceipt(paymentHash)
this.promptPrintType(paymentHash)
}
ws.close()
}
Expand Down Expand Up @@ -1133,6 +1177,18 @@ window.app = Vue.createApp({
this.paymentDetails.exchangeRate = data.extra.details.exchangeRate
}
return
} catch (error) {
console.error('Error showing print options:', error)
Quasar.Notify.create({
type: 'negative',
message: 'Error showing print options.'
})
}
},
promptPrintType(paymentHash) {
try {
this.printDialog.paymentHash = paymentHash
this.printDialog.show = true
} catch (error) {
console.error('Error fetching receipt data:', error)
Quasar.Notify.create({
Expand All @@ -1141,6 +1197,10 @@ window.app = Vue.createApp({
})
}
},
closePrintDialog() {
this.printDialog.show = false
this.printDialog.paymentHash = null
},
async printReceipt(paymentHash) {
try {
if (!this.receiptData) {
Expand All @@ -1151,17 +1211,32 @@ window.app = Vue.createApp({
this.receiptData = data
}

this.$q
.dialog({
title: 'Print Receipt',
message: 'Do you want to print the receipt?',
cancel: true,
persistent: false
})
.onOk(() => {
console.log('Printing receipt for payment hash:', paymentHash)
window.print()
})
this.orderReceipt = false
console.log('Printing receipt for payment hash:', paymentHash)
await this.$nextTick()
window.print()
} catch (error) {
console.error('Error fetching receipt data:', error)
Quasar.Notify.create({
type: 'negative',
message: 'Error fetching receipt data.'
})
}
},
async printOrderReceipt(paymentHash) {
try {
if (!this.receiptData) {
const {data} = await LNbits.api.request(
'GET',
`/tpos/api/v1/tposs/${this.tposId}/invoices/${paymentHash}?extra=true`
)
this.receiptData = data
}

this.orderReceipt = true
console.log('Printing order receipt for payment hash:', paymentHash)
await this.$nextTick()
window.print()
} catch (error) {
console.error('Error fetching receipt data:', error)
Quasar.Notify.create({
Expand Down
Loading