diff --git a/migrations.py b/migrations.py index c4ea3fb..1ff6a7b 100644 --- a/migrations.py +++ b/migrations.py @@ -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; + """ + ) diff --git a/models.py b/models.py index 7d4e956..a2c0da4 100644 --- a/models.py +++ b/models.py @@ -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) @@ -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 @@ -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 diff --git a/services.py b/services.py index eda5610..ae26b2a 100644 --- a/services.py +++ b/services.py @@ -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 diff --git a/static/components/order-receipt.js b/static/components/order-receipt.js new file mode 100644 index 0000000..111187c --- /dev/null +++ b/static/components/order-receipt.js @@ -0,0 +1,53 @@ +window.app.component('order-receipt', { + name: 'order-receipt', + props: ['data', 'type'], + data() {}, + template: ` +
+
+

Order

+
+ + + + +
+ ` +}) diff --git a/static/components/receipt.js b/static/components/receipt.js index 80dbd68..06e2a5e 100644 --- a/static/components/receipt.js +++ b/static/components/receipt.js @@ -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: { @@ -66,7 +77,9 @@ window.app.component('receipt', {

Receipt

-

+

+ +

@@ -79,17 +92,53 @@ window.app.component('receipt', {
+ +
@@ -119,7 +168,7 @@ window.app.component('receipt', {
-
+
Total (sats)
diff --git a/static/js/index.js b/static/js/index.js index a88a874..c97c5b0 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -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}` @@ -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: '' @@ -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: '' diff --git a/static/js/tpos.js b/static/js/tpos.js index c1b34c8..b01f327 100644 --- a/static/js/tpos.js +++ b/static/js/tpos.js @@ -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, @@ -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) @@ -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) @@ -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 @@ -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 @@ -837,7 +881,7 @@ window.app = Vue.createApp({ this.clearCart() this.showComplete() if (this.enablePrint) { - this.printReceipt(paymentHash) + this.promptPrintType(paymentHash) } ws.close() } @@ -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({ @@ -1141,6 +1197,10 @@ window.app = Vue.createApp({ }) } }, + closePrintDialog() { + this.printDialog.show = false + this.printDialog.paymentHash = null + }, async printReceipt(paymentHash) { try { if (!this.receiptData) { @@ -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({ diff --git a/tasks.py b/tasks.py index be80e72..be3de17 100644 --- a/tasks.py +++ b/tasks.py @@ -1,7 +1,5 @@ import asyncio -from loguru import logger - from lnbits.core.models import Payment from lnbits.core.services import ( create_invoice, @@ -10,6 +8,7 @@ websocket_updater, ) from lnbits.tasks import register_invoice_listener +from loguru import logger from .crud import get_tpos from .services import deduct_inventory_stock diff --git a/templates/tpos/_cart.html b/templates/tpos/_cart.html index 35fa99b..6b6fa2b 100644 --- a/templates/tpos/_cart.html +++ b/templates/tpos/_cart.html @@ -38,7 +38,16 @@
max-width: 1px; " > - + +
diff --git a/templates/tpos/dialogs.html b/templates/tpos/dialogs.html index 691f3ee..f515ce6 100644 --- a/templates/tpos/dialogs.html +++ b/templates/tpos/dialogs.html @@ -54,6 +54,34 @@
+ + + +
Print Receipt
+ + +
+ + + Print order receipt + + + Print receipt + + +
+
+
@@ -205,6 +233,11 @@
+ align="right" v-if="enablePrint && payment.amount > 0" > + + Print order receipt + - +
Payment Method
diff --git a/templates/tpos/index.html b/templates/tpos/index.html index 9216632..faa99c8 100644 --- a/templates/tpos/index.html +++ b/templates/tpos/index.html @@ -521,6 +521,10 @@
{{SITE_TITLE}} TPoS extension
label="VAT ID" placeholder="123456789" > +