/** @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"; import { formatCurrency as webFormatCurrency } from "@web/core/currency"; ClosePosPopup.props.push("last_cashier_name?"); const getCurrencyId = (pos) => { if (!pos) { return undefined; } if (pos.currency && typeof pos.currency === "object" && "id" in pos.currency) { return pos.currency.id; } if (pos.currency && typeof pos.currency === "number") { return pos.currency; } const configCurrency = pos.config?.currency_id; if (configCurrency) { if (typeof configCurrency === "object") { if ("id" in configCurrency) { return configCurrency.id; } if (Array.isArray(configCurrency)) { return configCurrency[0]; } } else if (typeof configCurrency === "number") { return configCurrency; } } return undefined; }; const safeFormatCurrency = (value, pos, hasSymbol = true) => { return webFormatCurrency(value, getCurrencyId(pos), { noSymbol: !hasSymbol, }); }; // 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(); // ── Open print window synchronously to avoid pop-up blocker ────────── let printWindow = null; try { printWindow = window.open("", "_blank", "width=400,height=600"); if (!printWindow) { const notification = this.pos?.env?.services?.notification; if (notification) { notification.add( _t("Pop-up blocked. Please allow pop-ups for this site to print the closing receipt."), { type: "warning" } ); } else { alert("Pop-up blocked. Please allow pop-ups for this site to print the closing receipt."); } } else { printWindow.document.write("

Closing Session, please wait...

"); } } catch (e) { console.error("[pos_closing_receipt] Failed to open print window", e); } const syncSuccess = await this.pos.pushOrdersWithClosingPopup(); if (!syncSuccess) { if (printWindow) printWindow.close(); 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) { if (printWindow) printWindow.close(); 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.") { if (printWindow) printWindow.close(); 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)]); 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) { if (printWindow) printWindow.close(); return this.handleClosingError(response); } // ── Print AFTER closing session on server ──────────────────────── await this._printClosingReceipt(receiptData, printWindow); // Mark session closed locally this.pos.session.state = "closed"; // ── Navigate away ──────────────────────────────────────────────── this.pos.router.close(); } catch (error) { if (printWindow) printWindow.close(); if (error instanceof ConnectionLostError) { throw error; } else { await this.handleClosingControlError(); } } finally { localStorage.removeItem(`pos.session.${odoo.pos_config_id}`); } }, async _printClosingReceipt(receiptData, printWindow) { 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 { let cashRow = ""; if (receiptData.cashPayment) { cashRow = ` ${receiptData.cashPayment.name} ${receiptData.cashPayment.formattedAmount} `; } const nonCashRows = (receiptData.nonCashPayments || []).map( (pm) => ` ${pm.name} ${pm.formattedAmount} ` ); let cashControlBlock = ""; if (receiptData.hasCashControl) { cashControlBlock = `
Expected Cash ${receiptData.expectedCash}
Counted Cash ${receiptData.countedCash}
Cash Difference ${receiptData.cashDifference}
`; } const html = ` Closing Summary
${receiptData.sessionName}
SESSION CLOSING SUMMARY
Cashier ${receiptData.cashierName}
Date/Time ${receiptData.closingTime}
${cashRow} ${nonCashRows.join("")}
TOTAL ${receiptData.grandTotal}
${cashControlBlock}
 
`; if (!printWindow) { return; } printWindow.document.open(); printWindow.document.write(html); printWindow.document.close(); // Safety delay to allow window to open and print dialog to spawn before proceeding await new Promise((resolve) => setTimeout(resolve, 1500)); } 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 = (val, hasSymbol = true) => safeFormatCurrency(val, pos, hasSymbol); let cashierName = this.props.last_cashier_name || ""; if (!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); let expectedCash = 0; let countedCash = 0; let cashDifference = 0; let hasCashControl = false; if (this.props.default_cash_details) { hasCashControl = true; expectedCash = this.props.default_cash_details.amount || 0; const countedStr = this.state.payments[this.props.default_cash_details.id]?.counted || "0"; countedCash = parseFloat(countedStr) || 0; cashDifference = countedCash - expectedCash; } return { sessionName, cashierName, closingTime, cashPayment, nonCashPayments, grandTotal, hasCashControl, expectedCash: formatCurrency(expectedCash), countedCash: formatCurrency(countedCash), cashDifference: formatCurrency(cashDifference), rawCashDifference: cashDifference, }; }, }); // ───────────────────────────────────────────────────────────────────────────── // 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 pos = this.pos; const formatCurrency = (val, hasSymbol = true) => safeFormatCurrency(val, pos, hasSymbol); // ── Open print window synchronously to avoid pop-up blocker ────────── let printWindow = null; try { 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; } else { printWindow.document.write("

Fetching Closing Summary...

"); } } catch (e) { console.error("[pos_closing_receipt] Failed to open print window", e); } // ── 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) { if (printWindow) printWindow.close(); this.notification.add(_t("No closed session found to reprint."), { type: "warning", }); return; } sessionData = result; } catch (err) { if (printWindow) printWindow.close(); 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); let cashControlBlock = ""; if (sessionData.has_cash_control) { cashControlBlock = `
Expected Cash ${formatCurrency(sessionData.closing_cash_expected)}
Counted Cash ${formatCurrency(sessionData.closing_cash_counted)}
Cash Difference ${formatCurrency(sessionData.closing_cash_difference)}
`; } // ── 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}
${cashControlBlock}
 
`; if (!printWindow) { return; } printWindow.document.open(); printWindow.document.write(html); printWindow.document.close(); }, });