/** @odoo-module **/
import { ClosePosPopup } from "@point_of_sale/app/components/popups/closing_popup/closing_popup";
import { Navbar } from "@point_of_sale/app/components/navbar/navbar";
import { patch } from "@web/core/utils/patch";
import { useService } from "@web/core/utils/hooks";
import { Component } from "@odoo/owl";
import { parseFloat } from "@web/views/fields/parsers";
import { ConnectionLostError } from "@web/core/network/rpc";
import { _t } from "@web/core/l10n/translation";
import { PrinterService } from "@point_of_sale/app/services/printer_service";
import { waitImages } from "@point_of_sale/utils";
// Patch PrinterService.printWeb to return a promise that resolves only after print dialog is closed
patch(PrinterService.prototype, {
async printWeb(el) {
console.log("[pos_closing_receipt] Patched printWeb called");
await this.renderer.whenMounted({
el,
callback: async (elClone) => {
console.log("[pos_closing_receipt] printWeb whenMounted callback started. Awaiting images...");
await waitImages(elClone);
console.log("[pos_closing_receipt] Images loaded. Invoking window.print...");
window.print(elClone);
console.log("[pos_closing_receipt] window.print has returned.");
},
});
console.log("[pos_closing_receipt] Patched printWeb fully resolved and returning true.");
return true;
}
});
// ─────────────────────────────────────────────────────────────────────────────
// ClosingReceipt OWL Component
// Rendered by the printer service (same way OrderReceipt is rendered).
// ─────────────────────────────────────────────────────────────────────────────
export class ClosingReceipt extends Component {
static template = "pos_closing_receipt.ClosingReceipt";
static props = {
sessionName: String,
cashierName: String,
closingTime: String,
cashPayment: { type: Object, optional: true },
nonCashPayments: Array,
grandTotal: String,
};
}
// ─────────────────────────────────────────────────────────────────────────────
// Patch ClosePosPopup: print closing receipt on session close
//
// WHY we override closeSession() instead of wrapping super.closeSession():
// The original ends with `this.pos.router.close()` → window.location.href,
// a hard browser redirect. Any code after `await super.closeSession()` is
// dead code. We inline the same logic so we can print BEFORE the redirect.
// ─────────────────────────────────────────────────────────────────────────────
patch(ClosePosPopup.prototype, {
setup() {
super.setup(...arguments);
this.printer = useService("printer");
},
async closeSession() {
this.pos._resetConnectedCashier();
const syncSuccess = await this.pos.pushOrdersWithClosingPopup();
if (!syncSuccess) {
return;
}
if (this.pos.config.cash_control) {
const response = await this.pos.data.call(
"pos.session",
"post_closing_cash_details",
[this.pos.session.id],
{
counted_cash: parseFloat(
this.state.payments[this.props.default_cash_details.id].counted
),
}
);
if (!response.successful) {
return this.handleClosingError(response);
}
}
try {
await this.pos.data.call(
"pos.session",
"update_closing_control_state_session",
[this.pos.session.id, this.state.notes]
);
} catch (error) {
if (!error.data && error.data?.message !== "This session is already closed.") {
throw error;
}
}
// ── Fetch real session name before the server closes it ──────────────
let realSessionName = this.pos.config.name || "POS Session";
try {
const result = await this.pos.data.read(
"pos.session",
[this.pos.session.id],
["name"]
);
if (result && result[0] && result[0].name && result[0].name !== "/") {
realSessionName = result[0].name;
}
} catch (_) {}
// ── Capture receipt data while props/pos data are still valid ────────
const receiptData = this._buildReceiptData(realSessionName);
try {
const bankPaymentMethodDiffPairs = this.props.non_cash_payment_methods
.filter((pm) => pm.type === "bank")
.map((pm) => [pm.id, this.getDifference(pm.id)]);
// ── Print BEFORE closing session on server ───────────────────────
// IMPORTANT: Must print here while IndexedDB is still fully open.
// After close_session_from_ui responds, the Odoo bus/sync detects
// the session state change and begins IDB teardown. Any OWL render
// or Logger.log call after that point throws:
// InvalidStateError: Failed to execute 'transaction' on
// 'IDBDatabase': The database connection is closing.
await this._printClosingReceipt(receiptData);
const response = await this.pos.data.call(
"pos.session",
"close_session_from_ui",
[this.pos.session.id, bankPaymentMethodDiffPairs],
{
context: {
device_identifier: this.pos.device.identifier,
},
}
);
if (!response.successful) {
return this.handleClosingError(response);
}
// Mark session closed locally
this.pos.session.state = "closed";
// ── Navigate away ────────────────────────────────────────────────
this.pos.router.close();
} catch (error) {
if (error instanceof ConnectionLostError) {
throw error;
} else {
await this.handleClosingControlError();
}
} finally {
localStorage.removeItem(`pos.session.${odoo.pos_config_id}`);
}
},
async _printClosingReceipt(receiptData) {
console.log("[pos_closing_receipt] Initiating closing receipt print...");
if (this.pos.env.services.ui) {
console.log("[pos_closing_receipt] Blocking UI during print...");
this.pos.env.services.ui.block();
}
try {
const printResult = await this.printer.print(ClosingReceipt, receiptData, {
webPrintFallback: true,
});
console.log("[pos_closing_receipt] Print operation resolved with result:", printResult);
// Explicit safety delay to let printer buffers flush (especially for Bluetooth/JSPM)
const delayMs = 1500;
console.log(`[pos_closing_receipt] Applying safety buffer delay of ${delayMs}ms...`);
await new Promise((resolve) => setTimeout(resolve, delayMs));
console.log("[pos_closing_receipt] Safety buffer complete.");
} catch (err) {
console.error("[pos_closing_receipt] CRITICAL: Failed to print closing receipt:", err);
} finally {
if (this.pos.env.services.ui) {
console.log("[pos_closing_receipt] Unblocking UI.");
this.pos.env.services.ui.unblock();
}
}
},
_buildReceiptData(sessionName) {
const pos = this.pos;
const formatCurrency = this.env.utils.formatCurrency;
let cashierName = "";
try {
const cashier = pos.getCashier();
cashierName = cashier?.name || cashier?.display_name || "";
} catch (_) {
cashierName = pos.user?.name || "";
}
const now = new Date();
const pad = (n) => String(n).padStart(2, "0");
const closingTime =
`${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ` +
`${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
let cashPayment = null;
if (this.props.default_cash_details) {
const amount = this.props.default_cash_details.payment_amount || 0;
cashPayment = {
id: this.props.default_cash_details.id,
name: this.props.default_cash_details.name || "Cash",
amount,
formattedAmount: formatCurrency(amount),
};
}
const nonCashPayments = (this.props.non_cash_payment_methods || []).map((pm) => ({
id: pm.id,
name: pm.name,
amount: pm.amount || 0,
formattedAmount: formatCurrency(pm.amount || 0),
}));
const cashAmount = cashPayment ? cashPayment.amount : 0;
const nonCashTotal = nonCashPayments.reduce((sum, pm) => sum + pm.amount, 0);
const grandTotal = formatCurrency(cashAmount + nonCashTotal);
return {
sessionName,
cashierName,
closingTime,
cashPayment,
nonCashPayments,
grandTotal,
};
},
});
// ─────────────────────────────────────────────────────────────────────────────
// Patch Navbar: add "Reprint Closing Summary" to the hamburger menu
//
// Fetches the last closed session for the current POS config from the server,
// builds receipt data, and prints with webPrintFallback.
// ─────────────────────────────────────────────────────────────────────────────
patch(Navbar.prototype, {
async reprintLastClosingReceipt() {
const formatCurrency = this.env.utils.formatCurrency;
// ── 1. Fetch the last closed session for this config ─────────────────
let sessionData;
try {
const result = await this.pos.data.call(
"pos.session",
"get_last_closed_session_summary",
[this.pos.config.id]
);
if (!result) {
this.notification.add(_t("No closed session found to reprint."), {
type: "warning",
});
return;
}
sessionData = result;
} catch (err) {
console.error("[pos_closing_receipt] Could not fetch last session:", err);
this.notification.add(_t("Failed to fetch session data."), { type: "danger" });
return;
}
// ── 2. Build formatted payment rows ──────────────────────────────────
let cashRow = "";
const nonCashRows = [];
for (const pm of sessionData.payment_methods) {
const formatted = formatCurrency(pm.amount);
const row = `
| ${pm.name} |
${formatted} |
`;
if (pm.is_cash) {
cashRow = row;
} else {
nonCashRows.push(row);
}
}
const cashAmount = (sessionData.payment_methods.find((p) => p.is_cash) || {}).amount || 0;
const nonCashTotal = sessionData.payment_methods
.filter((p) => !p.is_cash)
.reduce((s, p) => s + p.amount, 0);
const grandTotal = formatCurrency(cashAmount + nonCashTotal);
// ── 3. Build standalone HTML receipt and open print window ───────────
const html = `
Closing Summary Reprint
| Cashier |
${sessionData.cashier_name} |
| Date/Time |
${sessionData.closing_time} |
${cashRow}
${nonCashRows.join("")}
`;
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();
},
});