qz printer error handling and format updated

This commit is contained in:
Alaguraj0361 2026-04-28 10:10:37 +05:30
parent 1cccf85702
commit 2115f20e95
10 changed files with 237 additions and 117 deletions

View File

@ -4,7 +4,7 @@
height: 100vh !important;
width: 100vw !important;
overflow: hidden;
background: url('/dine360_theme_chennora/static/src/img/chen-banner-2.webp') !important;
background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)), url('/dine360_theme_chennora/static/src/img/chen-banner-2.webp') !important;
background-repeat: no-repeat !important;
background-position: center center !important;
background-size: cover !important;
@ -111,8 +111,8 @@
}
.form-control {
background: rgba(255, 255, 255, 0.05) !important;
border: 1px solid rgba(255, 255, 255, 0.1) !important;
background: rgba(255, 255, 255, 0.1) !important;
border: 1px solid rgba(255, 255, 255, 0.3) !important;
border-radius: 12px !important;
padding: 12px 15px !important;
height: auto !important;
@ -120,7 +120,14 @@
}
.form-control::placeholder {
color: rgba(255, 255, 255, 0.3) !important;
color: rgba(255, 255, 255, 0.6) !important;
}
/* Accessibility contrast for error messages */
.alert-danger {
background-color: rgba(220, 53, 69, 0.9) !important;
color: white !important;
border: none !important;
}
/* Login Button with Gradient */
@ -157,7 +164,16 @@
.o_login_right_side {
background: #0f172a !important;
/* Keep dark background on mobile */
}
.o_login_card_wrapper {
width: 90% !important;
}
.oe_website_login_container .oe_login_form,
.oe_website_login_container .oe_signup_form,
.oe_website_login_container .oe_reset_password_form {
width: 90% !important;
}
}

View File

@ -106,17 +106,7 @@
object-fit: contain !important;
}
/* "Rush Mode" Label next to logo */
.pos .pos-logo::after {
content: "RUSH MODE";
font-size: 14px;
font-weight: 800;
color: #ef4444;
letter-spacing: 2px;
border-left: 2px solid #e5e7eb;
padding-left: 20px;
margin-left: 10px;
}
/* "Rush Mode" Label removed for production */
/* 3. Search Bar - Teal Theme */
.pos .search-bar {
@ -365,7 +355,8 @@
.pos-receipt .pos-receipt-order-data {
color: #94a3b8 !important;
font-size: 11px !important;
font-size: 12px !important;
margin-top: 10px !important;
}
.pos-receipt .receipt-orderlines {
@ -375,11 +366,27 @@
.pos-receipt .orderline {
border: none !important;
margin: 5px 0 !important;
padding: 10px 0 !important;
margin: 0 !important;
padding: 8px 0 !important;
border-bottom: 1px dashed #e2e8f0 !important;
display: flex !important;
flex-wrap: wrap !important;
justify-content: space-between !important;
align-items: baseline !important;
}
.pos-receipt .orderline .product-name {
flex: 1 1 65% !important;
font-weight: 700 !important;
white-space: normal !important;
word-break: break-word !important;
padding-right: 5px !important;
}
.pos-receipt .orderline .pos-receipt-right-align {
flex: 0 0 auto !important;
text-align: right !important;
font-weight: 800 !important;
}
.pos-receipt .pos-receipt-total {
@ -392,4 +399,48 @@
.pos-receipt-amount {
font-weight: 800 !important;
}
/* ========================================
RESPONSIVE MEDIA QUERIES
======================================== */
@media (max-width: 1024px) {
.pos .leftpane {
width: 380px !important;
}
.pos .product {
width: 160px !important;
height: 180px !important;
}
.pos .product .product-img {
height: 110px !important;
}
.pos .search-bar {
width: 280px !important;
}
}
@media (max-width: 768px) {
.product-screen {
flex-direction: column !important;
height: 100vh !important;
}
.pos .leftpane {
width: 100% !important;
height: 50vh !important;
border-left: none !important;
border-top: 2px solid #f1f5f9 !important;
}
.pos .rightpane {
height: 50vh !important;
}
.pos .pos-topheader {
height: auto !important;
flex-wrap: wrap !important;
padding: 10px !important;
}
.pos .search-bar {
width: 100% !important;
margin-top: 10px !important;
}
}

View File

@ -19,14 +19,6 @@
<i class="fa fa-globe"/>
</a>
<a href="#" class="o_top_item" title="AI Assistant">
<span class="o_ai_icon">AI</span>
</a>
<a href="#" class="o_top_item" title="Search">
<i class="fa fa-search"/>
</a>
<a href="/web#action=mail.action_discuss" class="o_top_item" title="Messages">
<i class="fa fa-comments-o"/>
<span class="badge_dot"/>

View File

@ -3,14 +3,14 @@
/* ============================================ */
.online-orders-screen {
background: #1a1a2e;
background: #171422;
color: #eee;
font-family: 'Inter', 'Segoe UI', sans-serif;
}
/* Header */
.online-orders-header {
background: linear-gradient(135deg, #16213e 0%, #0f3460 100%);
background: #171422;
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
min-height: 60px;
}
@ -58,7 +58,7 @@
/* Body */
.online-orders-body {
background: #1a1a2e;
background: #171422;
}
/* Left Panel - Orders List */
@ -87,9 +87,9 @@
}
.order-card.selected {
background: rgba(15, 52, 96, 0.5);
border-color: #e94560;
box-shadow: 0 0 15px rgba(233, 69, 96, 0.2);
background: rgba(214, 17, 30, 0.1);
border-color: #d6111e;
box-shadow: 0 0 15px rgba(214, 17, 30, 0.2);
}
.order-ref {
@ -164,7 +164,7 @@
width: 44px;
height: 44px;
border-radius: 50%;
background: linear-gradient(135deg, #e94560, #0f3460);
background: linear-gradient(135deg, #d6111e, #1a1d23);
display: flex;
align-items: center;
justify-content: center;
@ -229,8 +229,8 @@
}
.kitchen-badge.active {
background: rgba(233, 69, 96, 0.2);
color: #e94560;
background: rgba(214, 17, 30, 0.2);
color: #d6111e;
}
/* Detail Action Buttons */
@ -265,9 +265,9 @@
/* Navbar Button */
.online-orders-nav-btn {
background: rgba(233, 69, 96, 0.15);
border: 1px solid rgba(233, 69, 96, 0.3);
color: #e94560;
background: rgba(214, 17, 30, 0.15);
border: 1px solid rgba(214, 17, 30, 0.3);
color: #d6111e;
border-radius: 10px;
padding: 6px 14px;
font-weight: 600;
@ -276,9 +276,9 @@
}
.online-orders-nav-btn:hover {
background: #e94560;
background: #d6111e;
color: #fff;
border-color: #e94560;
border-color: #d6111e;
}
/* Scrollbar */
@ -300,7 +300,7 @@
/* Spinner */
.spinner-border {
color: #e94560 !important;
color: #d6111e !important;
}
/* Responsive */

View File

@ -25,6 +25,7 @@ export class OnlineOrdersScreen extends Component {
loading: true,
selectedOrder: null,
confirmingId: null,
error: null,
});
console.log("[OnlineOrders] Services obtained:", {
@ -83,6 +84,7 @@ export class OnlineOrdersScreen extends Component {
async loadOnlineOrders() {
try {
this.state.loading = true;
this.state.error = null;
const orders = await this.orm.call(
"pos.order",
"get_online_orders",
@ -94,6 +96,7 @@ export class OnlineOrdersScreen extends Component {
} catch (error) {
console.error("[OnlineOrders] Error loading orders:", error);
this.state.loading = false;
this.state.error = "Failed to load orders. Please check your connection.";
}
}
@ -104,6 +107,7 @@ export class OnlineOrdersScreen extends Component {
async confirmOrder(orderId) {
try {
this.state.confirmingId = orderId;
this.state.error = null;
await this.orm.call(
"pos.order",
"action_confirm_online_order",
@ -119,12 +123,14 @@ export class OnlineOrdersScreen extends Component {
} catch (error) {
console.error("[OnlineOrders] Confirm error:", error);
this.notification.add("Failed to confirm order");
this.state.error = "Failed to confirm order. It might have been modified.";
this.state.confirmingId = null;
}
}
async rejectOrder(orderId) {
try {
this.state.error = null;
await this.orm.call(
"pos.order",
"action_reject_online_order",
@ -138,6 +144,7 @@ export class OnlineOrdersScreen extends Component {
} catch (error) {
console.error("[OnlineOrders] Reject error:", error);
this.notification.add("Failed to reject order");
this.state.error = "Failed to reject order.";
}
}

View File

@ -24,7 +24,21 @@
</div>
<!-- Content -->
<div class="online-orders-body flex-grow-1 d-flex overflow-hidden">
<div class="online-orders-body flex-grow-1 d-flex overflow-hidden position-relative">
<!-- Error Alert -->
<div t-if="state.error" class="position-absolute w-100 p-3" style="z-index: 1050; top: 0;">
<div class="alert alert-danger d-flex justify-content-between align-items-center m-0 shadow">
<div>
<i class="fa fa-exclamation-triangle me-2"/>
<span t-esc="state.error"/>
</div>
<button class="btn btn-sm btn-outline-danger border-0" t-on-click="() => this.state.error = null">
<i class="fa fa-times"/>
</button>
</div>
</div>
<!-- Loading State -->
<div t-if="state.loading" class="d-flex align-items-center justify-content-center w-100">
<div class="text-center">

View File

@ -20,25 +20,26 @@ export class ChannelPanel extends Component {
this.dialog = useService("dialog");
this.SOURCE_LABELS = {
walk_in: '🚶 Walk-In',
phone: '📞 Phone',
whatsapp: '💬 WhatsApp',
social_media: '📱 Social',
online: '🌐 Online',
kiosk: '📟 Kiosk',
qr: '📷 QR Code',
platform: '🛒 Platform',
walk_in: 'Walk-In',
phone: 'Phone',
whatsapp: 'WhatsApp',
social_media: 'Social',
online: 'Online',
kiosk: 'Kiosk',
qr: 'QR Code',
platform: 'Platform',
};
this.FULFILMENT_LABELS = {
dine_in: '🍽️ Dine-In',
pickup: '🛍️ Pickup',
delivery: '🚚 Delivery',
dine_in: 'Dine-In',
pickup: 'Pickup',
delivery: 'Delivery',
};
this.state = useState({
showDelivery: false,
showDetails: false, // For manual fields toggle
isCollapsed: false, // Panel collapse state
searchQuery: '',
searchResults: [],
searching: false,

View File

@ -4,11 +4,20 @@
<!-- ChannelPanel Component -->
<t t-name="dine360_order_channels.ChannelPanel" owl="1">
<t t-if="showPanel and currentOrder">
<div class="channel-panel d-flex flex-column gap-2 p-2 border-bottom">
<div class="channel-panel p-2 border-bottom">
<!-- Header with Collapse Toggle -->
<div class="d-flex justify-content-between align-items-center mb-1" style="cursor: pointer;" t-on-click="() => this.state.isCollapsed = !this.state.isCollapsed">
<span class="fw-bold small text-muted"><i class="fa fa-cogs me-1"/> Order Channels</span>
<button class="btn btn-sm btn-link p-0 text-decoration-none shadow-none text-muted">
<i t-attf-class="fa #{this.state.isCollapsed ? 'fa-chevron-down' : 'fa-chevron-up'}"/>
</button>
</div>
<div t-if="!this.state.isCollapsed" class="d-flex flex-column gap-2 mt-2">
<!-- Order Source Row -->
<div class="channel-section">
<div class="channel-label mb-1">📋 ORDER SOURCE</div>
<div class="channel-label mb-1"><i class="fa fa-list-alt me-1"></i> ORDER SOURCE</div>
<div class="d-flex flex-wrap gap-1">
<t t-foreach="Object.entries(SOURCE_LABELS)" t-as="entry" t-key="entry[0]">
<button
@ -23,7 +32,7 @@
<!-- Phone number field -->
<div t-if="orderSource === 'phone'" class="channel-extra">
<input type="text" class="form-control form-control-sm"
placeholder="📞 Phone Number"
placeholder="Phone Number"
t-att-value="currentOrder.telephone_number"
t-on-change="(ev) => { currentOrder.telephone_number = ev.target.value; }"/>
</div>
@ -31,7 +40,7 @@
<!-- WhatsApp number field -->
<div t-if="orderSource === 'whatsapp'" class="channel-extra">
<input type="text" class="form-control form-control-sm"
placeholder="📱 WhatsApp Number"
placeholder="WhatsApp Number"
t-att-value="currentOrder.whatsapp_number"
t-on-change="(ev) => { currentOrder.whatsapp_number = ev.target.value; }"/>
</div>
@ -39,14 +48,14 @@
<!-- Social media ref field -->
<div t-if="orderSource === 'social_media'" class="channel-extra">
<input type="text" class="form-control form-control-sm"
placeholder="📲 Post / Message Reference"
placeholder="Post / Message Reference"
t-att-value="currentOrder.social_ref"
t-on-change="(ev) => { currentOrder.social_ref = ev.target.value; }"/>
</div>
<!-- Fulfilment Row -->
<div class="channel-section mt-1">
<div class="channel-label mb-1">🚀 FULFILMENT TYPE</div>
<div class="channel-label mb-1"><i class="fa fa-rocket me-1"></i> FULFILMENT TYPE</div>
<div class="btn-group w-100" role="group">
<t t-foreach="Object.entries(FULFILMENT_LABELS)" t-as="entry" t-key="entry[0]">
<button
@ -125,6 +134,7 @@
</div>
</div>
</div>
</div>
</t>
</t>

View File

@ -1,30 +1,11 @@
/* Custom POS Navbar styling */
.pos .pos-topheader {
background: inherit;
}
.btn-dashboard:hover {
background: linear-gradient(135deg, #a00d0e 0%, #d61112 100%) !important;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3) !important;
}
/* Category Sidebar Red Background */
.pos .category-button {
background: #d61112 !important;
color: white !important;
border: 1px solid rgba(255, 255, 255, 0.1) !important;
transition: all 0.2s ease;
}
.pos .category-button:hover {
background: #a00d0e !important;
transform: scale(1.02);
}
.pos .category-button.active {
background: #111 !important;
/* Contrast active category */
box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.5) !important;
border: 1px solid #FECD4F !important;
}
/* Custom POS Navbar styling */
.pos .pos-topheader {
background: inherit;
}
.btn-dashboard:hover {
background: linear-gradient(135deg, #a00d0e 0%, #d61112 100%) !important;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3) !important;
}

View File

@ -52,48 +52,95 @@ function buildReceiptLinesFromOrder(order) {
const lines = [];
const company = order?.pos?.company;
const client = order?.get_partner?.();
const table = order?.table;
const cashier = order?.employee || order?.pos?.get_cashier?.();
// 1. HEADER (Centered-ish)
if (company?.name) {
lines.push(company.name);
lines.push(company.name.toUpperCase());
}
if (company?.phone) {
lines.push(company.phone);
if (company?.street) lines.push(company.street);
if (company?.city) lines.push(company.city);
if (company?.phone) lines.push(`Tel: ${company.phone}`);
// Custom Odoo Header
if (order?.pos?.config?.receipt_header) {
lines.push(order.pos.config.receipt_header);
}
lines.push("-".repeat(RECEIPT_COLUMNS));
const receiptNumber = order?.name || order?.uid || "";
if (receiptNumber) {
lines.push(`Receipt: ${receiptNumber}`);
// 2. ORDER INFO
const receiptNumber = order?.name || "";
if (receiptNumber) lines.push(`Order: ${receiptNumber}`);
if (table) {
lines.push(`TABLE: ${table.name}`.padEnd(20) + `GUESTS: ${order.customer_count || 1}`);
}
lines.push(new Date().toLocaleString());
if (client?.name) {
lines.push(`Customer: ${client.name}`);
if (cashier) {
lines.push(`SERVER: ${cashier.name}`);
}
lines.push(`DATE: ${new Date().toLocaleString()}`);
if (client) {
lines.push(`CUSTOMER: ${client.name}`);
}
lines.push("=".repeat(RECEIPT_COLUMNS));
lines.push(leftRight("ITEM", "PRICE"));
lines.push("-".repeat(RECEIPT_COLUMNS));
// 3. ORDER LINES
for (const orderline of order?.get_orderlines?.() || []) {
const product = orderline.get_product?.();
const name = product?.display_name || product?.name || "";
const qty = orderline.get_quantity?.() || 0;
const total = orderline.get_price_with_tax?.() ?? orderline.get_display_price?.() ?? 0;
for (const wrapped of wrapLine(name, RECEIPT_COLUMNS)) {
lines.push(wrapped);
const priceUnit = orderline.get_unit_display_price?.() || 0;
const total = orderline.get_price_with_tax?.() || 0;
// "Qty x Name" on left, "Total" on right
const itemLabel = `${qty} x ${name}`;
const itemPrice = money(total, currency);
if (itemLabel.length + itemPrice.length + 1 > RECEIPT_COLUMNS) {
lines.push(itemLabel);
lines.push(itemPrice.padStart(RECEIPT_COLUMNS));
} else {
lines.push(leftRight(itemLabel, itemPrice));
}
// Show unit price if qty > 1
if (qty > 1) {
lines.push(` @ ${money(priceUnit, currency)}`);
}
lines.push(leftRight(` ${qty} x`, money(total, currency)));
}
lines.push("-".repeat(RECEIPT_COLUMNS));
lines.push(leftRight("TOTAL", money(order?.get_total_with_tax?.(), currency)));
// 4. TOTALS
lines.push("=".repeat(RECEIPT_COLUMNS));
lines.push(leftRight("SUBTOTAL", money(order?.get_total_without_tax?.(), currency)));
const tax = order?.get_total_tax?.();
if (tax) {
lines.push(leftRight("Tax", money(tax, currency)));
lines.push(leftRight("TAX", money(tax, currency)));
}
lines.push("-".repeat(RECEIPT_COLUMNS));
lines.push(leftRight("TOTAL", money(order?.get_total_with_tax?.(), currency)));
const change = order?.get_change?.();
if (change) {
lines.push(leftRight("Change", money(change, currency)));
lines.push(leftRight("CHANGE", money(change, currency)));
}
lines.push("-".repeat(RECEIPT_COLUMNS));
lines.push("Thank you");
// Custom Odoo Footer
if (order?.pos?.config?.receipt_footer) {
lines.push(order.pos.config.receipt_footer);
}
lines.push("THANK YOU FOR DINING WITH US!");
lines.push("PLEASE VISIT AGAIN");
lines.push(NEWLINE);
return lines;
}
@ -108,7 +155,16 @@ function buildReceiptLines(receiptElement, order) {
function buildEscPosReceipt(receiptElement, order) {
const ESC = "\x1B";
const GS = "\x1D";
const lines = buildReceiptLines(receiptElement, order);
let lines = buildReceiptLines(receiptElement, order);
// Safety mechanism: limit receipt length to prevent runaway printing
if (lines.length > 150) {
console.warn("Receipt is suspiciously long, truncating to 150 lines to prevent runaway printing.");
lines.length = 150;
lines.push("-".repeat(RECEIPT_COLUMNS));
lines.push("TRUNCATED FOR SAFETY");
}
const body = lines.flatMap((line) => wrapLine(line)).join(NEWLINE);
return [
@ -123,14 +179,6 @@ function buildEscPosReceipt(receiptElement, order) {
patch(ReceiptScreen.prototype, {
async printReceipt() {
if (this.pos.config.use_qz_printer && this.pos.config.qz_printer_name) {
const rawPrintingEnabled = window.localStorage?.getItem("dine360_qz_raw_enabled") === "1";
if (!rawPrintingEnabled) {
console.warn(
"QZ raw receipt printing is disabled. Set localStorage.dine360_qz_raw_enabled = '1' only while testing a raw ESC/POS printer queue."
);
return super.printReceipt(...arguments);
}
try {
if (!window.qz) {
console.error("QZ Tray library not loaded.");