/** @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
${sessionData.session_name}
SESSION CLOSING SUMMARY
(REPRINT)
Cashier ${sessionData.cashier_name}
Date/Time ${sessionData.closing_time}
${cashRow} ${nonCashRows.join("")}
TOTAL ${grandTotal}
 
`; 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(); }, });