chore: bump version and update POS closing receipt logic

This commit is contained in:
Suherdy Yacob 2026-06-01 16:20:37 +07:00
parent a894acaefb
commit 33a69b76ce
3 changed files with 129 additions and 46 deletions

View File

@ -14,7 +14,7 @@ Odoo 19 custom module — automatically prints a **session closing summary** via
- Fault-tolerant: print failures are caught and logged; they never block the session closing process. - Fault-tolerant: print failures are caught and logged; they never block the session closing process.
- **Reprint**: adds a **Reprint Closing Summary** button on the closed session form in the Odoo backend. - **Reprint**: adds a **Reprint Closing Summary** button on the closed session form in the Odoo backend.
--- \---
## Receipt Layout ## Receipt Layout

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
{ {
'name': 'POS Closing Receipt Printer', 'name': 'POS Closing Receipt Printer',
'version': '19.0.1.3.0', 'version': '19.0.1.3.1',
'category': 'Point of Sale', 'category': 'Point of Sale',
'summary': 'Print payment summary receipt when closing a POS session, with reprint from backend', 'summary': 'Print payment summary receipt when closing a POS session, with reprint from backend',
'description': """ 'description': """

View File

@ -198,80 +198,163 @@ patch(ClosePosPopup.prototype, {
// builds receipt data, and prints with webPrintFallback. // builds receipt data, and prints with webPrintFallback.
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
patch(Navbar.prototype, { patch(Navbar.prototype, {
setup() {
super.setup(...arguments);
this.printer = useService("printer");
},
async reprintLastClosingReceipt() { async reprintLastClosingReceipt() {
const formatCurrency = this.env.utils.formatCurrency; const formatCurrency = this.env.utils.formatCurrency;
// ── 1. Fetch the last closed session for this config ───────────────── // ── 1. Fetch the last closed session for this config ─────────────────
let sessionData; let sessionData;
try { try {
const sessions = await this.pos.data.call( const result = await this.pos.data.call(
"pos.session", "pos.session",
"get_last_closed_session_summary", "get_last_closed_session_summary",
[this.pos.config.id] [this.pos.config.id]
); );
if (!sessions) { if (!result) {
this.notification.add(_t("No closed session found to reprint."), { this.notification.add(_t("No closed session found to reprint."), {
type: "warning", type: "warning",
}); });
return; return;
} }
sessionData = sessions; sessionData = result;
} catch (err) { } catch (err) {
console.error("[pos_closing_receipt] Could not fetch last session:", err); console.error("[pos_closing_receipt] Could not fetch last session:", err);
this.notification.add(_t("Failed to fetch session data."), { type: "danger" }); this.notification.add(_t("Failed to fetch session data."), { type: "danger" });
return; return;
} }
// ── 2. Build receipt props from server data ────────────────────────── // ── 2. Build formatted payment rows ──────────────────────────────────
let cashPayment = null; let cashRow = "";
const nonCashPayments = []; const nonCashRows = [];
for (const pm of sessionData.payment_methods) { for (const pm of sessionData.payment_methods) {
const formatted = formatCurrency(pm.amount);
const row = `
<tr>
<td style="padding:3px 0;">${pm.name}</td>
<td style="text-align:right;padding:3px 0;">${formatted}</td>
</tr>`;
if (pm.is_cash) { if (pm.is_cash) {
cashPayment = { cashRow = row;
id: pm.id,
name: pm.name,
amount: pm.amount,
formattedAmount: formatCurrency(pm.amount),
};
} else { } else {
nonCashPayments.push({ nonCashRows.push(row);
id: pm.id,
name: pm.name,
amount: pm.amount,
formattedAmount: formatCurrency(pm.amount),
});
} }
} }
const cashAmount = cashPayment ? cashPayment.amount : 0; const cashAmount = (sessionData.payment_methods.find((p) => p.is_cash) || {}).amount || 0;
const nonCashTotal = nonCashPayments.reduce((s, pm) => s + pm.amount, 0); const nonCashTotal = sessionData.payment_methods
.filter((p) => !p.is_cash)
.reduce((s, p) => s + p.amount, 0);
const grandTotal = formatCurrency(cashAmount + nonCashTotal); const grandTotal = formatCurrency(cashAmount + nonCashTotal);
const receiptData = { // ── 3. Build standalone HTML receipt and open print window ───────────
sessionName: sessionData.session_name, const html = `<!DOCTYPE html>
cashierName: sessionData.cashier_name, <html>
closingTime: sessionData.closing_time + " (REPRINT)", <head>
cashPayment, <meta charset="UTF-8"/>
nonCashPayments, <title>Closing Summary Reprint</title>
grandTotal, <style>
}; * { box-sizing: border-box; margin: 0; padding: 0; }
body {
// ── 3. Print ───────────────────────────────────────────────────────── font-family: 'Courier New', Courier, monospace;
try { font-size: 13px;
await this.printer.print(ClosingReceipt, receiptData, { background: #fff;
webPrintFallback: true, color: #000;
});
} catch (err) {
console.warn("[pos_closing_receipt] Reprint failed:", err);
this.notification.add(_t("Print failed. Check browser print settings."), {
type: "warning",
});
} }
.receipt {
width: 100%;
max-width: 320px;
margin: 0 auto;
padding: 12px;
}
.center { text-align: center; }
.header-title {
font-size: 16px;
font-weight: bold;
letter-spacing: 1px;
text-transform: uppercase;
}
.header-sub {
margin-top: 4px;
font-size: 11px;
letter-spacing: 2px;
}
.reprint-label {
margin-top: 2px;
font-size: 10px;
color: #666;
}
.dashed { border-top: 1px dashed #000; margin: 6px 0; }
.solid { border-top: 1px solid #000; margin: 6px 0; }
table { width: 100%; border-collapse: collapse; margin-bottom: 6px; }
td { padding: 3px 0; }
.right { text-align: right; }
.bold { font-weight: bold; }
.total-row td { font-weight: bold; font-size: 14px; padding: 4px 0; }
.footer {
text-align: center;
margin-top: 10px;
font-size: 11px;
letter-spacing: 1px;
}
.feed { margin-top: 24px; }
@media print {
@page { margin: 3mm; }
}
</style>
</head>
<body>
<div class="receipt">
<div class="center" style="margin-bottom:10px;">
<div class="header-title">${sessionData.session_name}</div>
<div class="header-sub">SESSION CLOSING SUMMARY</div>
<div class="reprint-label">(REPRINT)</div>
</div>
<div class="dashed"></div>
<table>
<tr>
<td class="bold">Cashier</td>
<td class="right">${sessionData.cashier_name}</td>
</tr>
<tr>
<td class="bold">Date/Time</td>
<td class="right" style="font-size:11px;">${sessionData.closing_time}</td>
</tr>
</table>
<div class="dashed"></div>
<table>
${cashRow}
${nonCashRows.join("")}
</table>
<div class="solid"></div>
<table>
<tr class="total-row">
<td>TOTAL</td>
<td class="right">${grandTotal}</td>
</tr>
</table>
<div class="dashed"></div>
<div class="footer">*** Session Closed ***</div>
<div class="feed">&nbsp;</div>
</div>
<script>
window.onload = function() {
window.print();
setTimeout(function() { window.close(); }, 1000);
};
</script>
</body>
</html>`;
const printWindow = window.open("", "_blank", "width=400,height=600");
if (!printWindow) {
this.notification.add(
_t("Pop-up blocked. Please allow pop-ups for this site and try again."),
{ type: "warning" }
);
return;
}
printWindow.document.open();
printWindow.document.write(html);
printWindow.document.close();
}, },
}); });