feat: print receipt
This commit is contained in:
@@ -12,6 +12,8 @@ export default function usePlaceOrder({
|
|||||||
const isPending = ref(false);
|
const isPending = ref(false);
|
||||||
const paidPaymentStatus = ref();
|
const paidPaymentStatus = ref();
|
||||||
const invoice = ref();
|
const invoice = ref();
|
||||||
|
const invoice_details = ref();
|
||||||
|
const payment_record = ref();
|
||||||
const showVietQRModal = ref();
|
const showVietQRModal = ref();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,12 +43,13 @@ export default function usePlaceOrder({
|
|||||||
delivery_method: orderInfo.value.deliveryMethod.id,
|
delivery_method: orderInfo.value.deliveryMethod.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
const invoice = await $insertapi("Invoice", {
|
const invoiceRecord = await $insertapi("Invoice", {
|
||||||
data: invoicePayload,
|
data: invoicePayload,
|
||||||
notify: false,
|
notify: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
return invoice;
|
invoice.value = invoiceRecord;
|
||||||
|
return invoiceRecord;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
$snackbar("Tạo đơn hàng không thành công", "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.
|
* Mark sold IMEIs, create invoice details, delivery info, payment records, reset cart.
|
||||||
* Runs when order payment is confirmed.
|
* Runs when order payment is confirmed.
|
||||||
@@ -67,7 +91,7 @@ export default function usePlaceOrder({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const imeisSold = await markImeisSold();
|
const imeisSold = await markImeisSold();
|
||||||
await createInvoiceDetails(invoiceId, imeisSold);
|
invoice_details.value = await createInvoiceDetails(invoiceId, imeisSold);
|
||||||
await createPaymentRecord(invoiceId);
|
await createPaymentRecord(invoiceId);
|
||||||
|
|
||||||
if (orderInfo.value.deliveryMethod.code === "HOME_DELIVERY") {
|
if (orderInfo.value.deliveryMethod.code === "HOME_DELIVERY") {
|
||||||
@@ -76,9 +100,7 @@ export default function usePlaceOrder({
|
|||||||
|
|
||||||
// update invoice.payment_status from PENDING to PAID
|
// update invoice.payment_status from PENDING to PAID
|
||||||
await $patchapi("Invoice", { id: invoiceId, payment_status: paidPaymentStatus.value.id });
|
await $patchapi("Invoice", { id: invoiceId, payment_status: paidPaymentStatus.value.id });
|
||||||
$snackbar("Khởi tạo đơn hàng thành công", "Success");
|
await onOrderSuccess();
|
||||||
resetCart();
|
|
||||||
onSuccess();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function markImeisSold() {
|
async function markImeisSold() {
|
||||||
@@ -110,6 +132,8 @@ export default function usePlaceOrder({
|
|||||||
const invoiceDetails = await $insertapi("Invoice_Detail", {
|
const invoiceDetails = await $insertapi("Invoice_Detail", {
|
||||||
data: invoiceDetailPayload,
|
data: invoiceDetailPayload,
|
||||||
notify: false,
|
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;
|
return invoiceDetails;
|
||||||
@@ -150,6 +174,7 @@ export default function usePlaceOrder({
|
|||||||
notify: false,
|
notify: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
payment_record.value = paymentRecord;
|
||||||
return paymentRecord;
|
return paymentRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +193,7 @@ export default function usePlaceOrder({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function placeOrder() {
|
async function placeOrder() {
|
||||||
invoice.value = await createOrder();
|
await createOrder();
|
||||||
|
|
||||||
if (orderInfo.value.paymentMethod.code === "CASH") {
|
if (orderInfo.value.paymentMethod.code === "CASH") {
|
||||||
finalizeOrder(invoice.value.id);
|
finalizeOrder(invoice.value.id);
|
||||||
|
|||||||
18
package-lock.json
generated
18
package-lock.json
generated
@@ -35,6 +35,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify-json/material-symbols": "^1.2.69",
|
"@iconify-json/material-symbols": "^1.2.69",
|
||||||
"@nuxt/icon": "^2.2.1",
|
"@nuxt/icon": "^2.2.1",
|
||||||
|
"@types/node": "^26.0.0",
|
||||||
"nuxt": "^4.4.7",
|
"nuxt": "^4.4.7",
|
||||||
"prettier": "3.8.3",
|
"prettier": "3.8.3",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3"
|
||||||
@@ -4293,6 +4294,16 @@
|
|||||||
"@types/unist": "*"
|
"@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": {
|
"node_modules/@types/resolve": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||||
@@ -12289,6 +12300,13 @@
|
|||||||
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
|
"integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/unenv": {
|
||||||
"version": "2.0.0-rc.24",
|
"version": "2.0.0-rc.24",
|
||||||
"resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz",
|
"resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify-json/material-symbols": "^1.2.69",
|
"@iconify-json/material-symbols": "^1.2.69",
|
||||||
"@nuxt/icon": "^2.2.1",
|
"@nuxt/icon": "^2.2.1",
|
||||||
|
"@types/node": "^26.0.0",
|
||||||
"nuxt": "^4.4.7",
|
"nuxt": "^4.4.7",
|
||||||
"prettier": "3.8.3",
|
"prettier": "3.8.3",
|
||||||
"typescript": "^6.0.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