feat: print receipt
This commit is contained in:
@@ -12,6 +12,8 @@ export default function usePlaceOrder({
|
||||
const isPending = ref(false);
|
||||
const paidPaymentStatus = ref();
|
||||
const invoice = ref();
|
||||
const invoice_details = ref();
|
||||
const payment_record = ref();
|
||||
const showVietQRModal = ref();
|
||||
|
||||
/**
|
||||
@@ -41,12 +43,13 @@ export default function usePlaceOrder({
|
||||
delivery_method: orderInfo.value.deliveryMethod.id,
|
||||
};
|
||||
|
||||
const invoice = await $insertapi("Invoice", {
|
||||
const invoiceRecord = await $insertapi("Invoice", {
|
||||
data: invoicePayload,
|
||||
notify: false,
|
||||
});
|
||||
|
||||
return invoice;
|
||||
invoice.value = invoiceRecord;
|
||||
return invoiceRecord;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
$snackbar("Tạo đơn hàng không thành công", "Error");
|
||||
@@ -55,6 +58,27 @@ export default function usePlaceOrder({
|
||||
}
|
||||
}
|
||||
|
||||
async function printReceipt() {
|
||||
return await $fetch("/api/print/receipt", {
|
||||
method: "POST",
|
||||
body: {
|
||||
invoice: invoice.value,
|
||||
invoice_details: invoice_details.value,
|
||||
payment_record: payment_record.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints receipt, displays snackbar, resets cart, closes modal
|
||||
*/
|
||||
async function onOrderSuccess() {
|
||||
$snackbar("Khởi tạo đơn hàng thành công", "Success");
|
||||
printReceipt();
|
||||
resetCart();
|
||||
onSuccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark sold IMEIs, create invoice details, delivery info, payment records, reset cart.
|
||||
* Runs when order payment is confirmed.
|
||||
@@ -67,7 +91,7 @@ export default function usePlaceOrder({
|
||||
}
|
||||
|
||||
const imeisSold = await markImeisSold();
|
||||
await createInvoiceDetails(invoiceId, imeisSold);
|
||||
invoice_details.value = await createInvoiceDetails(invoiceId, imeisSold);
|
||||
await createPaymentRecord(invoiceId);
|
||||
|
||||
if (orderInfo.value.deliveryMethod.code === "HOME_DELIVERY") {
|
||||
@@ -76,9 +100,7 @@ export default function usePlaceOrder({
|
||||
|
||||
// update invoice.payment_status from PENDING to PAID
|
||||
await $patchapi("Invoice", { id: invoiceId, payment_status: paidPaymentStatus.value.id });
|
||||
$snackbar("Khởi tạo đơn hàng thành công", "Success");
|
||||
resetCart();
|
||||
onSuccess();
|
||||
await onOrderSuccess();
|
||||
}
|
||||
|
||||
async function markImeisSold() {
|
||||
@@ -110,6 +132,8 @@ export default function usePlaceOrder({
|
||||
const invoiceDetails = await $insertapi("Invoice_Detail", {
|
||||
data: invoiceDetailPayload,
|
||||
notify: false,
|
||||
values:
|
||||
"id,code,invoice,variant,variant__code,variant__product,variant__product__name,variant__color,variant__color__name,variant__ram__code,variant__ram__capacity,variant__internal_storage__code,variant__internal_storage__capacity,variant__price,variant__image__path,imei_sold,price,status,note,deleted,create_time,update_time",
|
||||
});
|
||||
|
||||
return invoiceDetails;
|
||||
@@ -150,6 +174,7 @@ export default function usePlaceOrder({
|
||||
notify: false,
|
||||
});
|
||||
|
||||
payment_record.value = paymentRecord;
|
||||
return paymentRecord;
|
||||
}
|
||||
|
||||
@@ -168,7 +193,7 @@ export default function usePlaceOrder({
|
||||
}
|
||||
|
||||
async function placeOrder() {
|
||||
invoice.value = await createOrder();
|
||||
await createOrder();
|
||||
|
||||
if (orderInfo.value.paymentMethod.code === "CASH") {
|
||||
finalizeOrder(invoice.value.id);
|
||||
|
||||
18
package-lock.json
generated
18
package-lock.json
generated
@@ -35,6 +35,7 @@
|
||||
"devDependencies": {
|
||||
"@iconify-json/material-symbols": "^1.2.69",
|
||||
"@nuxt/icon": "^2.2.1",
|
||||
"@types/node": "^26.0.0",
|
||||
"nuxt": "^4.4.7",
|
||||
"prettier": "3.8.3",
|
||||
"typescript": "^6.0.3"
|
||||
@@ -4293,6 +4294,16 @@
|
||||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "26.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz",
|
||||
"integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~8.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||
@@ -12289,6 +12300,13 @@
|
||||
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
|
||||
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unenv": {
|
||||
"version": "2.0.0-rc.24",
|
||||
"resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"devDependencies": {
|
||||
"@iconify-json/material-symbols": "^1.2.69",
|
||||
"@nuxt/icon": "^2.2.1",
|
||||
"@types/node": "^26.0.0",
|
||||
"nuxt": "^4.4.7",
|
||||
"prettier": "3.8.3",
|
||||
"typescript": "^6.0.3"
|
||||
|
||||
71
print/print_receipt.py
Normal file
71
print/print_receipt.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import sys
|
||||
import json
|
||||
from datetime import datetime
|
||||
from escpos.printer import Win32Raw
|
||||
|
||||
LINE_WIDTH = 48 # 80mm paper
|
||||
|
||||
def safe_float(v):
|
||||
return float(v) if v is not None else 0.0
|
||||
|
||||
def money(n):
|
||||
return f"{int(safe_float(n)):,}"
|
||||
|
||||
def space_between(left, right, width=LINE_WIDTH):
|
||||
space = max(1, width - len(str(left)) - len(str(right)))
|
||||
return f"{left}{' ' * space}{right}"
|
||||
|
||||
def print_receipt(data, printer_name="XP-80C"):
|
||||
p = Win32Raw(printer_name=printer_name)
|
||||
|
||||
# Big bold header
|
||||
p.set(align='center', bold=True, double_height=True, double_width=True)
|
||||
p.text("ERP\n")
|
||||
|
||||
p.set(align='center', bold=False, normal_textsize=True)
|
||||
p.text(f"Invoice: {data['code']}\n")
|
||||
p.text(f"{data['ordered_at'][:10]}\n")
|
||||
p.text("-" * LINE_WIDTH + "\n")
|
||||
|
||||
p.set(align='left')
|
||||
p.text(f"Customer: {data['customer_name']}\n")
|
||||
p.text(f"Phone: {data['customer_phone']}\n")
|
||||
p.text(f"Address: {data['shipping_address']}\n")
|
||||
p.text("-" * LINE_WIDTH + "\n")
|
||||
|
||||
for item in data['items']:
|
||||
p.text(f"{item['name']}\n")
|
||||
p.text(f" {item['variant']}\n")
|
||||
p.text(space_between("", money(item['price'])) + "\n")
|
||||
|
||||
p.text("-" * LINE_WIDTH + "\n")
|
||||
|
||||
p.text(space_between("Subtotal:", money(data['product_amount'])) + "\n")
|
||||
if safe_float(data['shipping_fee']) > 0:
|
||||
p.text(space_between("Shipping:", money(data['shipping_fee'])) + "\n")
|
||||
if safe_float(data['discount_amount']) > 0:
|
||||
p.text(space_between("Discount:", f"-{money(data['discount_amount'])}") + "\n")
|
||||
|
||||
p.set(bold=True)
|
||||
p.text(space_between("TOTAL:", money(data['final_amount']) + " VND") + "\n")
|
||||
p.set(bold=False)
|
||||
|
||||
p.text("-" * LINE_WIDTH + "\n")
|
||||
|
||||
if safe_float(data['cash_amount']) > 0:
|
||||
p.text(space_between("Cash:", money(data['cash_amount'])) + "\n")
|
||||
if safe_float(data['transfer_amount']) > 0:
|
||||
p.text(space_between("Transfer:", money(data['transfer_amount'])) + "\n")
|
||||
|
||||
p.text("\n\n")
|
||||
p.set(align='center')
|
||||
now_str = datetime.now().strftime("%H:%M %d/%m/%Y")
|
||||
p.text(f"{now_str}\n")
|
||||
|
||||
p.text("\n" * 3)
|
||||
p.cut()
|
||||
|
||||
if __name__ == "__main__":
|
||||
data = json.loads(sys.argv[1])
|
||||
print_receipt(data)
|
||||
print("Sent to printer.")
|
||||
13
print/receipt_template.txt
Normal file
13
print/receipt_template.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
{big_title}
|
||||
{header}
|
||||
{line}
|
||||
{customer_block}
|
||||
{line}
|
||||
{items}
|
||||
{line}
|
||||
{totals}
|
||||
{line}
|
||||
{payment}
|
||||
{line_break}
|
||||
{footer_time}
|
||||
{blanks}
|
||||
43
server/api/print/receipt.post.ts
Normal file
43
server/api/print/receipt.post.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { execFile } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import path from "path";
|
||||
import { cloneDeepWith, deburr } from "es-toolkit";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
const bodyNonAccent = cloneDeepWith(body, (value) => (typeof value === "string" ? deburr(value) : undefined));
|
||||
const { invoice, invoice_details, payment_record } = bodyNonAccent;
|
||||
|
||||
const items = invoice_details.map((d: any) => ({
|
||||
name: d.variant__product__name,
|
||||
variant: `${d.variant__color__name} ${d.variant__ram__code}/${d.variant__internal_storage__code}`,
|
||||
imei: d.imei_sold,
|
||||
price: d.price,
|
||||
}));
|
||||
|
||||
const payload = JSON.stringify({
|
||||
code: invoice.code,
|
||||
ordered_at: invoice.ordered_at,
|
||||
customer_name: invoice.customer_name,
|
||||
customer_phone: invoice.customer_phone,
|
||||
shipping_address: invoice.shipping_address,
|
||||
items,
|
||||
product_amount: invoice.product_amount,
|
||||
shipping_fee: invoice.shipping_fee,
|
||||
discount_amount: invoice.discount_amount,
|
||||
final_amount: invoice.final_amount,
|
||||
cash_amount: payment_record.cash_amount,
|
||||
transfer_amount: payment_record.transfer_amount,
|
||||
});
|
||||
|
||||
const scriptPath = path.join(process.cwd(), "print", "print_receipt.py");
|
||||
|
||||
try {
|
||||
const { stdout } = await execFileAsync("python", [scriptPath, payload]);
|
||||
return { success: true, output: stdout };
|
||||
} catch (err) {
|
||||
throw createError({ statusCode: 500, statusMessage: "Print failed", data: String(err) });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user