implement Uber integration, website customization, KDS enhancements, and POS receipt extensions
This commit is contained in:
parent
eea113d2d5
commit
8d00d3a5dc
@ -75,23 +75,25 @@ patch(PosStore.prototype, {
|
|||||||
console.log(`[KDS] Setting up channel: ${channel}`);
|
console.log(`[KDS] Setting up channel: ${channel}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const busService = this.env.services.bus_service;
|
const busService = this.bus_service || this.env?.services?.bus_service;
|
||||||
busService.addChannel(channel);
|
if (busService) {
|
||||||
console.log("[KDS] Channel added successfully");
|
busService.addChannel(channel);
|
||||||
|
console.log("[KDS] Channel added successfully");
|
||||||
|
|
||||||
busService.addEventListener("notification", (event) => {
|
busService.addEventListener("notification", (event) => {
|
||||||
console.log("[KDS] *** NOTIFICATION RECEIVED ***", event);
|
console.log("[KDS] *** NOTIFICATION RECEIVED ***", event);
|
||||||
const notifications = event.detail || [];
|
const notifications = event.detail || [];
|
||||||
|
|
||||||
for (const notif of notifications) {
|
for (const notif of notifications) {
|
||||||
if (notif.type === 'kds_update') {
|
if (notif.type === 'kds_update') {
|
||||||
console.log("[KDS] *** KDS UPDATE ***", notif.payload);
|
console.log("[KDS] *** KDS UPDATE ***", notif.payload);
|
||||||
this._handleKdsUpdate(notif.payload);
|
this._handleKdsUpdate(notif.payload);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
console.log("[KDS] Listener registered successfully");
|
console.log("[KDS] Listener registered successfully");
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[KDS] ERROR:", error);
|
console.error("[KDS] ERROR:", error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -182,16 +182,38 @@ class SaleOrderOnline(models.Model):
|
|||||||
], order='id desc', limit=1)
|
], order='id desc', limit=1)
|
||||||
|
|
||||||
if pos_order:
|
if pos_order:
|
||||||
pos_order.write({
|
pos_vals = {
|
||||||
'is_online_order': True,
|
'is_online_order': True,
|
||||||
'online_order_status': 'pending',
|
'online_order_status': 'pending',
|
||||||
'sale_order_id': sale_order.id,
|
'sale_order_id': sale_order.id,
|
||||||
'online_order_date': fields.Datetime.now(),
|
'online_order_date': fields.Datetime.now(),
|
||||||
'order_source': sale_order.order_source or 'online',
|
'order_source': sale_order.order_source or 'online',
|
||||||
'fulfilment_type': sale_order.fulfilment_type or 'pickup',
|
'fulfilment_type': sale_order.fulfilment_type or 'pickup',
|
||||||
# 'delivery_time': sale_order.delivery_time,
|
}
|
||||||
# 'uber_eta': sale_order.delivery_time,
|
|
||||||
})
|
# Check if this is an Uber delivery order
|
||||||
|
is_uber = False
|
||||||
|
if sale_order.fulfilment_type == 'delivery':
|
||||||
|
is_uber = True
|
||||||
|
elif sale_order.carrier_id and 'uber' in (sale_order.carrier_id.name or '').lower():
|
||||||
|
is_uber = True
|
||||||
|
elif any('uber' in (l.product_id.name or '').lower() or 'delivery' in (l.product_id.name or '').lower() for l in sale_order.order_line if getattr(l, 'is_delivery', False)):
|
||||||
|
is_uber = True
|
||||||
|
|
||||||
|
if is_uber:
|
||||||
|
pos_vals['is_uber_order'] = True
|
||||||
|
pos_vals['delivery_type'] = 'uber'
|
||||||
|
shipping = sale_order.partner_shipping_id or sale_order.partner_id
|
||||||
|
if shipping:
|
||||||
|
pos_vals['delivery_partner_id'] = shipping.id
|
||||||
|
pos_vals['delivery_street'] = f"{shipping.street or ''} {shipping.street2 or ''}".strip()
|
||||||
|
pos_vals['delivery_city'] = shipping.city or ''
|
||||||
|
pos_vals['delivery_zip'] = shipping.zip or ''
|
||||||
|
pos_vals['delivery_phone'] = shipping.phone or shipping.mobile or ''
|
||||||
|
if hasattr(sale_order, 'amount_delivery') and sale_order.amount_delivery > 0:
|
||||||
|
pos_vals['uber_delivery_fee'] = sale_order.amount_delivery
|
||||||
|
|
||||||
|
pos_order.write(pos_vals)
|
||||||
|
|
||||||
# Link back to sale order
|
# Link back to sale order
|
||||||
sale_order.write({'pos_order_id': pos_order.id})
|
sale_order.write({'pos_order_id': pos_order.id})
|
||||||
|
|||||||
@ -4,17 +4,11 @@ import { patch } from "@web/core/utils/patch";
|
|||||||
import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen";
|
import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen";
|
||||||
import { ChannelPanel } from "./channel_panel";
|
import { ChannelPanel } from "./channel_panel";
|
||||||
|
|
||||||
/**
|
// Register ChannelPanel directly on ProductScreen components
|
||||||
* Patch ProductScreen to:
|
ProductScreen.components = {
|
||||||
* 1. Register ChannelPanel as a component
|
...ProductScreen.components,
|
||||||
* 2. Expose showChannelPanel computed property to the template
|
ChannelPanel,
|
||||||
*/
|
};
|
||||||
patch(ProductScreen, {
|
|
||||||
components: {
|
|
||||||
...ProductScreen.components,
|
|
||||||
ChannelPanel,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
patch(ProductScreen.prototype, {
|
patch(ProductScreen.prototype, {
|
||||||
get showChannelPanel() {
|
get showChannelPanel() {
|
||||||
|
|||||||
@ -1,268 +1,38 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<templates xml:space="preserve">
|
<templates xml:space="preserve">
|
||||||
|
|
||||||
|
<!-- Non-destructive extension of OrderReceipt to display Channel and Delivery details -->
|
||||||
<t t-name="dine360_order_channels.OrderReceipt" t-inherit="point_of_sale.OrderReceipt" t-inherit-mode="extension" owl="1">
|
<t t-name="dine360_order_channels.OrderReceipt" t-inherit="point_of_sale.OrderReceipt" t-inherit-mode="extension" owl="1">
|
||||||
<xpath expr="//div[hasclass('pos-receipt')]" position="replace">
|
<xpath expr="//div[hasclass('before-footer')]" position="before">
|
||||||
<div class="pos-receipt custom-restaurant-receipt">
|
<div class="channel-info border-top pt-2 mt-2" style="font-size: 12px; font-family: inherit;">
|
||||||
<style>
|
<div t-if="props.data.order_source" class="d-flex justify-content-between mb-1">
|
||||||
.custom-restaurant-receipt {
|
<span>Order Source:</span>
|
||||||
width: 100%;
|
<span class="fw-bold text-uppercase" t-esc="props.data.order_source_label"/>
|
||||||
font-family: 'Arial', sans-serif;
|
</div>
|
||||||
color: #333;
|
<div t-if="props.data.fulfilment_type" class="d-flex justify-content-between mb-1">
|
||||||
background: #fff;
|
<span>Fulfilment:</span>
|
||||||
padding: 10px 5px;
|
<span class="fw-bold text-uppercase" t-esc="props.data.fulfilment_type_label"/>
|
||||||
}
|
|
||||||
.custom-restaurant-receipt .main-title {
|
|
||||||
color: #E67E22;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0 0 15px 0;
|
|
||||||
}
|
|
||||||
.custom-restaurant-receipt .receipt-header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.custom-restaurant-receipt .receipt-header img {
|
|
||||||
max-width: 120px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.custom-restaurant-receipt .company-name {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 16px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
.custom-restaurant-receipt .company-details {
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
.custom-restaurant-receipt .subtitle {
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: center;
|
|
||||||
margin: 15px 0;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.custom-restaurant-receipt .info-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.custom-restaurant-receipt .info-box {
|
|
||||||
background: #FDEBD0;
|
|
||||||
padding: 6px 10px;
|
|
||||||
border-radius: 2px;
|
|
||||||
flex: 1;
|
|
||||||
margin: 0 4px;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.custom-restaurant-receipt .info-box span {
|
|
||||||
font-weight: normal;
|
|
||||||
display: block;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
.custom-restaurant-receipt .info-box:first-child { margin-left: 0; }
|
|
||||||
.custom-restaurant-receipt .info-box:last-child { margin-right: 0; }
|
|
||||||
|
|
||||||
.custom-restaurant-receipt table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.custom-restaurant-receipt th {
|
|
||||||
background: #E67E22;
|
|
||||||
color: white;
|
|
||||||
padding: 8px 6px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
.custom-restaurant-receipt th:first-child { text-align: left; }
|
|
||||||
.custom-restaurant-receipt td {
|
|
||||||
padding: 8px 6px;
|
|
||||||
text-align: right;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
}
|
|
||||||
.custom-restaurant-receipt td:first-child { text-align: left; }
|
|
||||||
|
|
||||||
.custom-restaurant-receipt .totals-section {
|
|
||||||
width: 70%;
|
|
||||||
float: right;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
.custom-restaurant-receipt .totals-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 6px 0;
|
|
||||||
}
|
|
||||||
.custom-restaurant-receipt .totals-row.bold {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.custom-restaurant-receipt .totals-row .val-box {
|
|
||||||
background: #FDEBD0;
|
|
||||||
padding: 4px 10px;
|
|
||||||
min-width: 80px;
|
|
||||||
text-align: right;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
.custom-restaurant-receipt .clearfix::after {
|
|
||||||
content: "";
|
|
||||||
clear: both;
|
|
||||||
display: table;
|
|
||||||
}
|
|
||||||
.custom-restaurant-receipt .footer-slogan {
|
|
||||||
text-align: center;
|
|
||||||
font-weight: bold;
|
|
||||||
font-style: italic;
|
|
||||||
font-size: 16px;
|
|
||||||
margin-top: 20px;
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
.custom-restaurant-receipt .channel-info {
|
|
||||||
font-size: 11px;
|
|
||||||
margin-top: 15px;
|
|
||||||
border-top: 1px dashed #ccc;
|
|
||||||
padding-top: 10px;
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="main-title">Restaurant Receipt</div>
|
|
||||||
|
|
||||||
<div class="receipt-header">
|
|
||||||
<img t-attf-src="/web/image?model=res.company&id={{props.data.headerData.company.id}}&field=logo" alt="Logo"/>
|
|
||||||
<div class="company-name" t-esc="props.data.headerData.company.name"/>
|
|
||||||
<div class="company-details">
|
|
||||||
<div t-if="props.data.headerData.company.contact_address" t-esc="props.data.headerData.company.contact_address"/>
|
|
||||||
<div>
|
|
||||||
<t t-if="props.data.headerData.company.phone">Tel: <t t-esc="props.data.headerData.company.phone"/> </t>
|
|
||||||
<t t-if="props.data.headerData.company.email"> | <t t-esc="props.data.headerData.company.email"/></t>
|
|
||||||
</div>
|
|
||||||
<div t-if="props.data.headerData.company.website" t-esc="props.data.headerData.company.website"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="subtitle">"Authentic Indian Food At its Finest!"</div>
|
<t t-if="props.data.fulfilment_type === 'delivery'">
|
||||||
|
<div class="mt-2 pt-2 border-top">
|
||||||
|
<div class="fw-bold mb-1">DELIVERY ADDRESS:</div>
|
||||||
|
<div t-if="props.data.delivery_street" t-esc="props.data.delivery_street"/>
|
||||||
|
<div t-if="props.data.delivery_city || props.data.delivery_zip">
|
||||||
|
<t t-esc="props.data.delivery_city"/> <t t-esc="props.data.delivery_zip"/>
|
||||||
|
</div>
|
||||||
|
<div t-if="props.data.delivery_phone">Phone: <t t-esc="props.data.delivery_phone"/></div>
|
||||||
|
<div t-if="props.data.delivery_notes" class="mt-1 small" style="font-style: italic;">
|
||||||
|
Note: <t t-esc="props.data.delivery_notes"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
<div class="info-row">
|
<div t-if="props.data.social_ref" class="mt-1 small">
|
||||||
<div class="info-box">
|
Ref: <t t-esc="props.data.social_ref"/>
|
||||||
Receipt No.
|
|
||||||
<span t-esc="props.data.name"/>
|
|
||||||
</div>
|
|
||||||
<div class="info-box">
|
|
||||||
Date & Time
|
|
||||||
<span t-esc="props.data.date"/>
|
|
||||||
</div>
|
|
||||||
<div class="info-box" t-if="props.data.headerData.cashier">
|
|
||||||
Cashier
|
|
||||||
<span t-esc="props.data.headerData.cashier"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div t-if="props.data.whatsapp_number" class="mt-1 small">
|
||||||
<table>
|
WhatsApp: <t t-esc="props.data.whatsapp_number"/>
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>List of Items</th>
|
|
||||||
<th>Qty</th>
|
|
||||||
<th>Unit Cost</th>
|
|
||||||
<th>Amount</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr t-foreach="props.data.orderlines" t-as="line" t-key="line.id">
|
|
||||||
<td>
|
|
||||||
<t t-esc="line.productName"/>
|
|
||||||
<div t-if="line.customerNote" class="small italic text-muted mt-1">
|
|
||||||
<i class="fa fa-sticky-note me-1"/> <t t-esc="line.customerNote"/>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td><t t-esc="line.qty"/></td>
|
|
||||||
<td><t t-esc="props.formatCurrency(line.price)"/></td>
|
|
||||||
<td><t t-esc="props.formatCurrency(line.price_display)"/></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<div class="clearfix">
|
|
||||||
<div class="totals-section">
|
|
||||||
<div class="totals-row">
|
|
||||||
<span>Total Amount</span>
|
|
||||||
<span class="val-box" t-esc="props.formatCurrency(props.data.total_without_tax)"/>
|
|
||||||
</div>
|
|
||||||
<div class="totals-row" t-if="props.data.amount_tax > 0">
|
|
||||||
<span>VAT</span>
|
|
||||||
<span class="val-box" t-esc="props.formatCurrency(props.data.amount_tax)"/>
|
|
||||||
</div>
|
|
||||||
<div class="totals-row bold">
|
|
||||||
<span>Net Amount</span>
|
|
||||||
<span class="val-box" t-esc="props.formatCurrency(props.data.amount_total)"/>
|
|
||||||
</div>
|
|
||||||
<t t-if="props.data.rounding_applied">
|
|
||||||
<div class="totals-row">
|
|
||||||
<span>Rounding</span>
|
|
||||||
<span class="val-box" t-esc="props.formatCurrency(props.data.rounding_applied)"/>
|
|
||||||
</div>
|
|
||||||
<div class="totals-row bold">
|
|
||||||
<span>To Pay</span>
|
|
||||||
<span class="val-box" t-esc="props.formatCurrency(props.data.amount_total + props.data.rounding_applied)"/>
|
|
||||||
</div>
|
|
||||||
</t>
|
|
||||||
|
|
||||||
<!-- Payment Lines -->
|
|
||||||
<div class="totals-row" t-foreach="props.data.paymentlines" t-as="line" t-key="line.name">
|
|
||||||
<span t-esc="line.name"/>
|
|
||||||
<span class="val-box" t-esc="props.formatCurrency(line.amount, false)"/>
|
|
||||||
</div>
|
|
||||||
<div class="totals-row" t-if="props.data.change > 0">
|
|
||||||
<span>Change</span>
|
|
||||||
<span class="val-box" t-esc="props.formatCurrency(props.data.change)"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="footer-slogan">Eat As much As You Like!</div>
|
|
||||||
|
|
||||||
<!-- Order Channels Info -->
|
|
||||||
<div class="channel-info">
|
|
||||||
<div t-if="props.data.order_source" class="d-flex justify-content-between mb-1">
|
|
||||||
<span>Order Source:</span>
|
|
||||||
<span class="fw-bold text-uppercase" t-esc="props.data.order_source_label"/>
|
|
||||||
</div>
|
|
||||||
<div t-if="props.data.fulfilment_type" class="d-flex justify-content-between mb-1">
|
|
||||||
<span>Fulfilment:</span>
|
|
||||||
<span class="fw-bold text-uppercase" t-esc="props.data.fulfilment_type_label"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<t t-if="props.data.fulfilment_type === 'delivery'">
|
|
||||||
<div class="mt-2 pt-2 border-top">
|
|
||||||
<div class="fw-bold mb-1">DELIVERY ADDRESS:</div>
|
|
||||||
<div t-if="props.data.delivery_street" t-esc="props.data.delivery_street"/>
|
|
||||||
<div t-if="props.data.delivery_city || props.data.delivery_zip">
|
|
||||||
<t t-esc="props.data.delivery_city"/> <t t-esc="props.data.delivery_zip"/>
|
|
||||||
</div>
|
|
||||||
<div t-if="props.data.delivery_phone">Phone: <t t-esc="props.data.delivery_phone"/></div>
|
|
||||||
<div t-if="props.data.delivery_notes" class="mt-1 small" style="font-style: italic;">
|
|
||||||
Note: <t t-esc="props.data.delivery_notes"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</t>
|
|
||||||
|
|
||||||
<div t-if="props.data.social_ref" class="mt-1 small">
|
|
||||||
Ref: <t t-esc="props.data.social_ref"/>
|
|
||||||
</div>
|
|
||||||
<div t-if="props.data.whatsapp_number" class="mt-1 small">
|
|
||||||
WhatsApp: <t t-esc="props.data.whatsapp_number"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pos-receipt-order-data text-center mt-3" t-if="props.data.footer" style="white-space:pre-line; font-size: 11px;">
|
|
||||||
<t t-esc="props.data.footer" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pos-receipt-order-data mt-3 text-center" style="font-size: 10px;">
|
|
||||||
<p>Powered by Dine360</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</xpath>
|
</xpath>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
/** @odoo-module */
|
/** @odoo-module */
|
||||||
|
|
||||||
import { patch } from "@web/core/utils/patch";
|
import { patch } from "@web/core/utils/patch";
|
||||||
import { ReceiptScreen } from "@point_of_sale/app/screens/receipt_screen/receipt_screen";
|
import { ReceiptScreen } from "@point_of_sale/app/screens/receipt_screen/receipt_screen";
|
||||||
@ -393,7 +393,10 @@ patch(ReceiptScreen.prototype, {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return super.printReceipt(...arguments);
|
if (typeof super.printReceipt === "function") {
|
||||||
|
return super.printReceipt(...arguments);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -445,8 +448,8 @@ async function buildEscPosKitchenTicket(order, pos, changes) {
|
|||||||
patch(PosStore.prototype, {
|
patch(PosStore.prototype, {
|
||||||
async sendOrderToPrinter(order) {
|
async sendOrderToPrinter(order) {
|
||||||
if (this.config.use_qz_printer && this.config.qz_kitchen_printer_name) {
|
if (this.config.use_qz_printer && this.config.qz_kitchen_printer_name) {
|
||||||
const changes = order.computeChanges();
|
const changes = typeof order.computeChanges === "function" ? order.computeChanges() : {};
|
||||||
if (Object.keys(changes.categories).length > 0) {
|
if (changes && changes.categories && Object.keys(changes.categories).length > 0) {
|
||||||
try {
|
try {
|
||||||
if (!window.qz) {
|
if (!window.qz) {
|
||||||
console.error("QZ Tray library not loaded.");
|
console.error("QZ Tray library not loaded.");
|
||||||
@ -472,11 +475,13 @@ patch(PosStore.prototype, {
|
|||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("QZ Tray Kitchen Print Error:", err);
|
console.error("QZ Tray Kitchen Print Error:", err);
|
||||||
// Fallback or show error
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return super.sendOrderToPrinter(...arguments);
|
if (typeof super.sendOrderToPrinter === "function") {
|
||||||
|
return super.sendOrderToPrinter(...arguments);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from odoo import models, fields
|
from odoo import models, fields, api
|
||||||
|
|
||||||
class ProductTemplate(models.Model):
|
class ProductTemplate(models.Model):
|
||||||
_inherit = 'product.template'
|
_inherit = 'product.template'
|
||||||
@ -9,3 +9,18 @@ class ProductTemplate(models.Model):
|
|||||||
default=False,
|
default=False,
|
||||||
help='Check this to show this product in Popular Deals section on Homepage'
|
help='Check this to show this product in Popular Deals section on Homepage'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
for vals in vals_list:
|
||||||
|
if 'name' in vals and vals.get('name'):
|
||||||
|
name = vals.get('name', '')
|
||||||
|
if not vals.get('website_meta_title'):
|
||||||
|
vals['website_meta_title'] = f"{name} | Shiva Sakthi Restaurant"
|
||||||
|
if not vals.get('website_meta_description'):
|
||||||
|
desc = vals.get('description_sale') or vals.get('description') or ''
|
||||||
|
if desc:
|
||||||
|
vals['website_meta_description'] = desc[:150]
|
||||||
|
else:
|
||||||
|
vals['website_meta_description'] = f"Enjoy delicious, authentic {name} at Shiva Sakthi Restaurant, Mississauga. Order online now for dine-in, take-out, or catering."
|
||||||
|
return super(ProductTemplate, self).create(vals_list)
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 15 KiB |
@ -1,4 +1,4 @@
|
|||||||
/* Global Font Family Overrides */
|
/* Global Font Family Overrides */
|
||||||
h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
|
h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
|
||||||
font-family: 'Bebas Neue', sans-serif !important;
|
font-family: 'Bebas Neue', sans-serif !important;
|
||||||
}
|
}
|
||||||
@ -576,9 +576,9 @@ section h2.display-4 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
border-color: rgba(43, 177, 165, 0.3) !important;
|
border-color: rgba(255, 184, 0, 0.5) !important;
|
||||||
background-color: white !important;
|
background-color: white !important;
|
||||||
box-shadow: 0 5px 15px rgba(43, 177, 165, 0.1) !important;
|
box-shadow: 0 5px 15px rgba(255, 184, 0, 0.15) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1576,7 +1576,7 @@ footer#bottom {
|
|||||||
|
|
||||||
/* Global Red Color Removal & Theme Alignment */
|
/* Global Red Color Removal & Theme Alignment */
|
||||||
.text-danger {
|
.text-danger {
|
||||||
color: #ffb800 !important; // Replace red text with theme teal
|
color: #ffb800 !important; // Replace red text with theme gold
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-danger,
|
.bg-danger,
|
||||||
@ -1597,7 +1597,7 @@ footer#bottom {
|
|||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(43, 177, 165, 0.1) !important;
|
background-color: rgba(255, 184, 0, 0.1) !important;
|
||||||
color: #ffb800 !important;
|
color: #ffb800 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1615,7 +1615,7 @@ footer#bottom {
|
|||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: white !important;
|
color: white !important;
|
||||||
background-color: #259a8f !important;
|
background-color: #e0a200 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1626,7 +1626,7 @@ footer#bottom {
|
|||||||
/* Link hover color */
|
/* Link hover color */
|
||||||
body:not(.editor_enable):not(.o_edit_mode) a:hover,
|
body:not(.editor_enable):not(.o_edit_mode) a:hover,
|
||||||
body:not(.editor_enable):not(.o_edit_mode) a:hover * {
|
body:not(.editor_enable):not(.o_edit_mode) a:hover * {
|
||||||
color: #529791 !important;
|
color: #ffb800 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Button hover color */
|
/* Button hover color */
|
||||||
@ -2398,9 +2398,9 @@ body.o_edit_mode {
|
|||||||
.enjoy-btn-outline {
|
.enjoy-btn-outline {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: 2px solid #ffb800;
|
border: 2px solid #ffb800 !important;
|
||||||
color: #ffb800;
|
color: #ffb800 !important;
|
||||||
background: transparent;
|
background: transparent !important;
|
||||||
padding: 10px 28px;
|
padding: 10px 28px;
|
||||||
font-family: 'Bebas Neue', sans-serif !important;
|
font-family: 'Bebas Neue', sans-serif !important;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
@ -2410,8 +2410,9 @@ body.o_edit_mode {
|
|||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #ffb800;
|
background: #ffb800 !important;
|
||||||
color: #111111;
|
color: #111111 !important;
|
||||||
|
border-color: #ffb800 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3187,5 +3188,9 @@ body.o_edit_mode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Enforce section title font size and weight */
|
/* Enforce section title font size and weight */
|
||||||
section h2.display-4, section h2.display-5, section h2.testi-main-title, section h2.thali-subtitle, section h2 { font-size: 56px !important; font-weight: 400 !important; }
|
section h2.display-4, section h2.display-5, section h2.testi-main-title, section h2.thali-subtitle, section h2 {
|
||||||
|
font-size: clamp(32px, 5vw, 56px) !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
line-height: 1.25 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -368,138 +368,182 @@
|
|||||||
|
|
||||||
<!-- Menu Card Container -->
|
<!-- Menu Card Container -->
|
||||||
<div class="menu-card-container mb-5">
|
<div class="menu-card-container mb-5">
|
||||||
<div class="row g-0">
|
<t t-if="pop_products">
|
||||||
<!-- Column 1 -->
|
<t t-set="left_products" t-value="pop_products[:int((len(pop_products) + 1) / 2)]"/>
|
||||||
<div class="col-lg-6 menu-column border-end-lg">
|
<t t-set="right_products" t-value="pop_products[int((len(pop_products) + 1) / 2):]"/>
|
||||||
<div class="d-flex flex-column gap-4 p-4 p-md-5">
|
<div class="row g-0">
|
||||||
<!-- Item 1 -->
|
<!-- Column 1 -->
|
||||||
<div class="menu-list-item d-flex align-items-center">
|
<div class="col-lg-6 menu-column border-end-lg">
|
||||||
<img src="/dine360_theme_shivasakthi/static/src/img/crispy-gobi-65.png" class="rounded-circle me-3" alt="Medhu Vada"/>
|
<div class="d-flex flex-column gap-4 p-4 p-md-5">
|
||||||
<div class="flex-grow-1">
|
<t t-foreach="left_products" t-as="product">
|
||||||
<div class="d-flex align-items-baseline">
|
<div class="menu-list-item d-flex align-items-center">
|
||||||
<h5 class="fw-bold mb-0 text-dark">MEDHU VADA</h5>
|
<img t-att-src="website.image_url(product, 'image_128') if product.image_128 else '/dine360_theme_shivasakthi/static/src/img/crispy-gobi-65.png'" class="rounded-circle me-3" t-att-alt="product.name" style="width: 75px; height: 75px; object-fit: cover; flex-shrink: 0;"/>
|
||||||
<span class="flex-grow-1 border-bottom mx-2"/>
|
<div class="flex-grow-1">
|
||||||
<span class="fw-bold price-text">$7.99</span>
|
<div class="d-flex align-items-baseline">
|
||||||
|
<h5 class="fw-bold mb-0 text-dark" t-out="product.name"/>
|
||||||
|
<span class="flex-grow-1 border-bottom mx-2"/>
|
||||||
|
<span class="fw-bold price-text" t-field="product.list_price" t-options="{'widget': 'monetary', 'display_currency': website.currency_id}"/>
|
||||||
|
</div>
|
||||||
|
<p class="small text-muted mb-0" t-if="product.description_sale" t-out="product.description_sale"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Column 2 -->
|
||||||
|
<div class="col-lg-6 menu-column">
|
||||||
|
<div class="d-flex flex-column gap-4 p-4 p-md-5">
|
||||||
|
<t t-foreach="right_products" t-as="product">
|
||||||
|
<div class="menu-list-item d-flex align-items-center">
|
||||||
|
<img t-att-src="website.image_url(product, 'image_128') if product.image_128 else '/dine360_theme_shivasakthi/static/src/img/crispy-gobi-65.png'" class="rounded-circle me-3" t-att-alt="product.name" style="width: 75px; height: 75px; object-fit: cover; flex-shrink: 0;"/>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="d-flex align-items-baseline">
|
||||||
|
<h5 class="fw-bold mb-0 text-dark" t-out="product.name"/>
|
||||||
|
<span class="flex-grow-1 border-bottom mx-2"/>
|
||||||
|
<span class="fw-bold price-text" t-field="product.list_price" t-options="{'widget': 'monetary', 'display_currency': website.currency_id}"/>
|
||||||
|
</div>
|
||||||
|
<p class="small text-muted mb-0" t-if="product.description_sale" t-out="product.description_sale"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<div class="row g-0">
|
||||||
|
<!-- Column 1 -->
|
||||||
|
<div class="col-lg-6 menu-column border-end-lg">
|
||||||
|
<div class="d-flex flex-column gap-4 p-4 p-md-5">
|
||||||
|
<!-- Item 1 -->
|
||||||
|
<div class="menu-list-item d-flex align-items-center">
|
||||||
|
<img src="/dine360_theme_shivasakthi/static/src/img/crispy-gobi-65.png" class="rounded-circle me-3" alt="Medhu Vada"/>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="d-flex align-items-baseline">
|
||||||
|
<h5 class="fw-bold mb-0 text-dark">MEDHU VADA</h5>
|
||||||
|
<span class="flex-grow-1 border-bottom mx-2"/>
|
||||||
|
<span class="fw-bold price-text">$7.99</span>
|
||||||
|
</div>
|
||||||
|
<p class="small text-muted mb-0">The popular soft and savory appetizer</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Item 2 -->
|
||||||
|
<div class="menu-list-item d-flex align-items-center">
|
||||||
|
<img src="/dine360_theme_shivasakthi/static/src/img/cat-dosa.png" class="rounded-circle me-3" alt="Garlic Naan"/>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="d-flex align-items-baseline">
|
||||||
|
<h5 class="fw-bold mb-0 text-dark">GARLIC NAAN</h5>
|
||||||
|
<span class="flex-grow-1 border-bottom mx-2"/>
|
||||||
|
<span class="fw-bold price-text">$1.99</span>
|
||||||
|
</div>
|
||||||
|
<p class="small text-muted mb-0">Naan infused with garlic</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Item 3 -->
|
||||||
|
<div class="menu-list-item d-flex align-items-center">
|
||||||
|
<img src="/dine360_theme_shivasakthi/static/src/img/fiery-fish-curry.png" class="rounded-circle me-3" alt="King Fish Fry"/>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="d-flex align-items-baseline">
|
||||||
|
<h5 class="fw-bold mb-0 text-dark">KING FISH FRY</h5>
|
||||||
|
<span class="flex-grow-1 border-bottom mx-2"/>
|
||||||
|
<span class="fw-bold price-text">$17.99</span>
|
||||||
|
</div>
|
||||||
|
<p class="small text-muted mb-0">House Special - Premium King Fish Slice Fry</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Item 4 -->
|
||||||
|
<div class="menu-list-item d-flex align-items-center">
|
||||||
|
<img src="/dine360_theme_shivasakthi/static/src/img/cat-desserts.png" class="rounded-circle me-3" alt="Paruppu Pradhaman"/>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="d-flex align-items-baseline">
|
||||||
|
<h5 class="fw-bold mb-0 text-dark">PARUPPU PRADHAMAN</h5>
|
||||||
|
<span class="flex-grow-1 border-bottom mx-2"/>
|
||||||
|
<span class="fw-bold price-text">$3.99</span>
|
||||||
|
</div>
|
||||||
|
<p class="small text-muted mb-0">south style Payasam made with coconut, Lentils and Jaggery</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Item 5 -->
|
||||||
|
<div class="menu-list-item d-flex align-items-center">
|
||||||
|
<img src="/dine360_theme_shivasakthi/static/src/img/crispy-gobi-65.png" class="rounded-circle me-3" alt="Egg Bhurji"/>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="d-flex align-items-baseline">
|
||||||
|
<h5 class="fw-bold mb-0 text-dark">EGG BHURJI (PODIMAS)</h5>
|
||||||
|
<span class="flex-grow-1 border-bottom mx-2"/>
|
||||||
|
<span class="fw-bold price-text">$11.99</span>
|
||||||
|
</div>
|
||||||
|
<p class="small text-muted mb-0">Masala Egg Bhurji (Podimas)</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="small text-muted mb-0">The popular soft and savory appetizer</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Item 2 -->
|
</div>
|
||||||
<div class="menu-list-item d-flex align-items-center">
|
<!-- Column 2 -->
|
||||||
<img src="/dine360_theme_shivasakthi/static/src/img/cat-dosa.png" class="rounded-circle me-3" alt="Garlic Naan"/>
|
<div class="col-lg-6 menu-column">
|
||||||
<div class="flex-grow-1">
|
<div class="d-flex flex-column gap-4 p-4 p-md-5">
|
||||||
<div class="d-flex align-items-baseline">
|
<!-- Item 1 -->
|
||||||
<h5 class="fw-bold mb-0 text-dark">GARLIC NAAN</h5>
|
<div class="menu-list-item d-flex align-items-center">
|
||||||
<span class="flex-grow-1 border-bottom mx-2"/>
|
<img src="/dine360_theme_shivasakthi/static/src/img/cat-biryani.png" class="rounded-circle me-3" alt="Veg Biryani"/>
|
||||||
<span class="fw-bold price-text">$1.99</span>
|
<div class="flex-grow-1">
|
||||||
|
<div class="d-flex align-items-baseline">
|
||||||
|
<h5 class="fw-bold mb-0 text-dark">VEG BIRYANI</h5>
|
||||||
|
<span class="flex-grow-1 border-bottom mx-2"/>
|
||||||
|
<span class="fw-bold price-text">$13.99</span>
|
||||||
|
</div>
|
||||||
|
<p class="small text-muted mb-0">Cooked in Chettinadu style using Basmati rice</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="small text-muted mb-0">Naan infused with garlic</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<!-- Item 2 -->
|
||||||
<!-- Item 3 -->
|
<div class="menu-list-item d-flex align-items-center">
|
||||||
<div class="menu-list-item d-flex align-items-center">
|
<img src="/dine360_theme_shivasakthi/static/src/img/cat-curries.png" class="rounded-circle me-3" alt="Hakka Noodles"/>
|
||||||
<img src="/dine360_theme_shivasakthi/static/src/img/fiery-fish-curry.png" class="rounded-circle me-3" alt="King Fish Fry"/>
|
<div class="flex-grow-1">
|
||||||
<div class="flex-grow-1">
|
<div class="d-flex align-items-baseline">
|
||||||
<div class="d-flex align-items-baseline">
|
<h5 class="fw-bold mb-0 text-dark">HAKKA NOODLES (VEG)</h5>
|
||||||
<h5 class="fw-bold mb-0 text-dark">KING FISH FRY</h5>
|
<span class="flex-grow-1 border-bottom mx-2"/>
|
||||||
<span class="flex-grow-1 border-bottom mx-2"/>
|
<span class="fw-bold price-text">$13.99</span>
|
||||||
<span class="fw-bold price-text">$17.99</span>
|
</div>
|
||||||
|
<p class="small text-muted mb-0">Hakka Flavored Noodles</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="small text-muted mb-0">House Special - Premium King Fish Slice Fry</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<!-- Item 3 -->
|
||||||
<!-- Item 4 -->
|
<div class="menu-list-item d-flex align-items-center">
|
||||||
<div class="menu-list-item d-flex align-items-center">
|
<img src="/dine360_theme_shivasakthi/static/src/img/cat-biryani.png" class="rounded-circle me-3" alt="Thalapakattu Mutton Biryani"/>
|
||||||
<img src="/dine360_theme_shivasakthi/static/src/img/cat-desserts.png" class="rounded-circle me-3" alt="Paruppu Pradhaman"/>
|
<div class="flex-grow-1">
|
||||||
<div class="flex-grow-1">
|
<div class="d-flex align-items-baseline">
|
||||||
<div class="d-flex align-items-baseline">
|
<h5 class="fw-bold mb-0 text-dark">THALAPAKATTU MUTTON BIRYANI</h5>
|
||||||
<h5 class="fw-bold mb-0 text-dark">PARUPPU PRADHAMAN</h5>
|
<span class="flex-grow-1 border-bottom mx-2"/>
|
||||||
<span class="flex-grow-1 border-bottom mx-2"/>
|
<span class="fw-bold price-text">$16.99</span>
|
||||||
<span class="fw-bold price-text">$3.99</span>
|
</div>
|
||||||
|
<p class="small text-muted mb-0">Authentic Thalapakattu style in seeraga samba rice</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="small text-muted mb-0">south style Payasam made with coconut, Lentils and Jaggery</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<!-- Item 4 -->
|
||||||
<!-- Item 5 -->
|
<div class="menu-list-item d-flex align-items-center">
|
||||||
<div class="menu-list-item d-flex align-items-center">
|
<img src="/dine360_theme_shivasakthi/static/src/img/cat-dosa.png" class="rounded-circle me-3" alt="Pizza Dosa"/>
|
||||||
<img src="/dine360_theme_shivasakthi/static/src/img/crispy-gobi-65.png" class="rounded-circle me-3" alt="Egg Bhurji"/>
|
<div class="flex-grow-1">
|
||||||
<div class="flex-grow-1">
|
<div class="d-flex align-items-baseline">
|
||||||
<div class="d-flex align-items-baseline">
|
<h5 class="fw-bold mb-0 text-dark">PIZZA DOSA</h5>
|
||||||
<h5 class="fw-bold mb-0 text-dark">EGG BHURJI (PODIMAS)</h5>
|
<span class="flex-grow-1 border-bottom mx-2"/>
|
||||||
<span class="flex-grow-1 border-bottom mx-2"/>
|
<span class="fw-bold price-text">$14.99</span>
|
||||||
<span class="fw-bold price-text">$11.99</span>
|
</div>
|
||||||
|
<p class="small text-muted mb-0">Classic Dosa made to taste like pizza and favorite with kids and adults alike</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Item 5 -->
|
||||||
|
<div class="menu-list-item d-flex align-items-center">
|
||||||
|
<img src="/dine360_theme_shivasakthi/static/src/img/cat-desserts.png" class="rounded-circle me-3" alt="Payasam"/>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="d-flex align-items-baseline">
|
||||||
|
<h5 class="fw-bold mb-0 text-dark">PAYASAM</h5>
|
||||||
|
<span class="flex-grow-1 border-bottom mx-2"/>
|
||||||
|
<span class="fw-bold price-text">$3.99</span>
|
||||||
|
</div>
|
||||||
|
<p class="small text-muted mb-0">Traditional Tamilnadu style vermichilli payasam</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="small text-muted mb-0">Masala Egg Bhurji (Podimas)</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Column 2 -->
|
</t>
|
||||||
<div class="col-lg-6 menu-column">
|
|
||||||
<div class="d-flex flex-column gap-4 p-4 p-md-5">
|
|
||||||
<!-- Item 1 -->
|
|
||||||
<div class="menu-list-item d-flex align-items-center">
|
|
||||||
<img src="/dine360_theme_shivasakthi/static/src/img/cat-biryani.png" class="rounded-circle me-3" alt="Veg Biryani"/>
|
|
||||||
<div class="flex-grow-1">
|
|
||||||
<div class="d-flex align-items-baseline">
|
|
||||||
<h5 class="fw-bold mb-0 text-dark">VEG BIRYANI</h5>
|
|
||||||
<span class="flex-grow-1 border-bottom mx-2"/>
|
|
||||||
<span class="fw-bold price-text">$13.99</span>
|
|
||||||
</div>
|
|
||||||
<p class="small text-muted mb-0">Cooked in Chettinadu style using Basmati rice</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Item 2 -->
|
|
||||||
<div class="menu-list-item d-flex align-items-center">
|
|
||||||
<img src="/dine360_theme_shivasakthi/static/src/img/cat-curries.png" class="rounded-circle me-3" alt="Hakka Noodles"/>
|
|
||||||
<div class="flex-grow-1">
|
|
||||||
<div class="d-flex align-items-baseline">
|
|
||||||
<h5 class="fw-bold mb-0 text-dark">HAKKA NOODLES (VEG)</h5>
|
|
||||||
<span class="flex-grow-1 border-bottom mx-2"/>
|
|
||||||
<span class="fw-bold price-text">$13.99</span>
|
|
||||||
</div>
|
|
||||||
<p class="small text-muted mb-0">Hakka Flavored Noodles</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Item 3 -->
|
|
||||||
<div class="menu-list-item d-flex align-items-center">
|
|
||||||
<img src="/dine360_theme_shivasakthi/static/src/img/cat-biryani.png" class="rounded-circle me-3" alt="Thalapakattu Mutton Biryani"/>
|
|
||||||
<div class="flex-grow-1">
|
|
||||||
<div class="d-flex align-items-baseline">
|
|
||||||
<h5 class="fw-bold mb-0 text-dark">THALAPAKATTU MUTTON BIRYANI</h5>
|
|
||||||
<span class="flex-grow-1 border-bottom mx-2"/>
|
|
||||||
<span class="fw-bold price-text">$16.99</span>
|
|
||||||
</div>
|
|
||||||
<p class="small text-muted mb-0">Authentic Thalapakattu style in seeraga samba rice</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Item 4 -->
|
|
||||||
<div class="menu-list-item d-flex align-items-center">
|
|
||||||
<img src="/dine360_theme_shivasakthi/static/src/img/cat-dosa.png" class="rounded-circle me-3" alt="Pizza Dosa"/>
|
|
||||||
<div class="flex-grow-1">
|
|
||||||
<div class="d-flex align-items-baseline">
|
|
||||||
<h5 class="fw-bold mb-0 text-dark">PIZZA DOSA</h5>
|
|
||||||
<span class="flex-grow-1 border-bottom mx-2"/>
|
|
||||||
<span class="fw-bold price-text">$14.99</span>
|
|
||||||
</div>
|
|
||||||
<p class="small text-muted mb-0">Classic Dosa made to taste like pizza and favorite with kids and adults alike</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Item 5 -->
|
|
||||||
<div class="menu-list-item d-flex align-items-center">
|
|
||||||
<img src="/dine360_theme_shivasakthi/static/src/img/cat-desserts.png" class="rounded-circle me-3" alt="Payasam"/>
|
|
||||||
<div class="flex-grow-1">
|
|
||||||
<div class="d-flex align-items-baseline">
|
|
||||||
<h5 class="fw-bold mb-0 text-dark">PAYASAM</h5>
|
|
||||||
<span class="flex-grow-1 border-bottom mx-2"/>
|
|
||||||
<span class="fw-bold price-text">$3.99</span>
|
|
||||||
</div>
|
|
||||||
<p class="small text-muted mb-0">Traditional Tamilnadu style vermichilli payasam</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- View All Button -->
|
<!-- View All Button -->
|
||||||
|
|||||||
@ -12,6 +12,15 @@
|
|||||||
<label for="is_popular_deal"/>
|
<label for="is_popular_deal"/>
|
||||||
</span>
|
</span>
|
||||||
</xpath>
|
</xpath>
|
||||||
|
<xpath expr="//notebook" position="inside">
|
||||||
|
<page name="seo" string="SEO">
|
||||||
|
<group name="seo_metadata" string="Search Engine Optimization">
|
||||||
|
<field name="website_meta_title" placeholder="e.g. Delicious Masala Dosa"/>
|
||||||
|
<field name="website_meta_description" placeholder="e.g. Crispy crepe filled with spiced potato..."/>
|
||||||
|
<field name="website_meta_keywords" placeholder="e.g. dosa, south indian, restaurant"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
- Real-time status updates between Odoo and Uber
|
- Real-time status updates between Odoo and Uber
|
||||||
""",
|
""",
|
||||||
'author': 'Dine360',
|
'author': 'Dine360',
|
||||||
'depends': ['point_of_sale', 'dine360_restaurant', 'dine360_kds', 'website_sale'],
|
'depends': ['point_of_sale', 'dine360_restaurant', 'dine360_kds', 'dine360_order_channels', 'website_sale', 'delivery'],
|
||||||
'data': [
|
'data': [
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'data/uber_cron_data.xml',
|
'data/uber_cron_data.xml',
|
||||||
|
|||||||
@ -7,30 +7,268 @@ _logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class UberWebhookController(http.Controller):
|
class UberWebhookController(http.Controller):
|
||||||
|
|
||||||
@http.route('/uber/webhook/delivery', type='json', auth='none', methods=['POST'], csrf=False)
|
@http.route('/uber/webhook/delivery', type='http', auth='none', methods=['POST'], csrf=False)
|
||||||
def uber_delivery_webhook(self, **post):
|
def uber_delivery_webhook(self, **post):
|
||||||
"""Handle status updates from Uber Direct"""
|
"""Handle status updates from Uber Direct (standard REST JSON webhook)"""
|
||||||
data = json.loads(request.httprequest.data)
|
try:
|
||||||
|
raw_body = request.httprequest.data
|
||||||
|
if isinstance(raw_body, bytes):
|
||||||
|
raw_body = raw_body.decode('utf-8')
|
||||||
|
data = json.loads(raw_body) if raw_body else {}
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error("Failed to parse Uber Webhook JSON: %s", str(e))
|
||||||
|
return request.make_json_response({'status': 'error', 'message': 'Invalid JSON'}, status=400)
|
||||||
|
|
||||||
_logger.info("Uber Webhook Received: %s", json.dumps(data, indent=2))
|
_logger.info("Uber Webhook Received: %s", json.dumps(data, indent=2))
|
||||||
|
|
||||||
uber_delivery_id = data.get('delivery_id')
|
# Extract delivery ID or order ID across various Uber webhook formats
|
||||||
status = data.get('status') # e.g., 'picked_up', 'delivered'
|
uber_id = (
|
||||||
|
data.get('delivery_id') or
|
||||||
|
data.get('id') or
|
||||||
|
data.get('order_id') or
|
||||||
|
(data.get('data', {}).get('id') if isinstance(data.get('data'), dict) else False) or
|
||||||
|
(data.get('meta', {}).get('resource_id') if isinstance(data.get('meta'), dict) else False)
|
||||||
|
)
|
||||||
|
|
||||||
if uber_delivery_id:
|
raw_status = (
|
||||||
order = request.env['pos.order'].sudo().search([('uber_delivery_id', '=', uber_delivery_id)], limit=1)
|
data.get('status') or
|
||||||
|
(data.get('data', {}).get('status') if isinstance(data.get('data'), dict) else False) or
|
||||||
|
data.get('event_type')
|
||||||
|
)
|
||||||
|
|
||||||
|
if uber_id:
|
||||||
|
order = request.env['pos.order'].sudo().search([
|
||||||
|
'|', ('uber_delivery_id', '=', str(uber_id)), ('uber_order_id', '=', str(uber_id))
|
||||||
|
], limit=1)
|
||||||
if order:
|
if order:
|
||||||
# Map Uber status to Odoo status
|
|
||||||
status_map = {
|
status_map = {
|
||||||
|
'pending': 'pending',
|
||||||
|
'created': 'pending',
|
||||||
'pickup': 'pickup',
|
'pickup': 'pickup',
|
||||||
|
'en_route_to_pickup': 'pickup',
|
||||||
|
'arrived_at_pickup': 'pickup',
|
||||||
|
'pickup_complete': 'delivering',
|
||||||
'pickup_completed': 'delivering',
|
'pickup_completed': 'delivering',
|
||||||
|
'en_route_to_dropoff': 'delivering',
|
||||||
|
'arrived_at_dropoff': 'delivering',
|
||||||
|
'delivering': 'delivering',
|
||||||
|
'delivery_complete': 'delivered',
|
||||||
'dropoff_completed': 'delivered',
|
'dropoff_completed': 'delivered',
|
||||||
'cancelled': 'cancelled'
|
'delivered': 'delivered',
|
||||||
|
'completed': 'delivered',
|
||||||
|
'canceled': 'cancelled',
|
||||||
|
'cancelled': 'cancelled',
|
||||||
|
'returned': 'cancelled',
|
||||||
}
|
}
|
||||||
order.uber_status = status_map.get(status, order.uber_status)
|
|
||||||
return {'status': 'success'}
|
|
||||||
|
|
||||||
return {'status': 'ignored'}
|
# Check for event_type format (e.g., 'deliveries.dropoff_completed')
|
||||||
|
if raw_status and '.' in str(raw_status):
|
||||||
|
raw_status = str(raw_status).split('.')[-1]
|
||||||
|
|
||||||
|
new_status = status_map.get(raw_status, order.uber_status)
|
||||||
|
|
||||||
|
vals = {'uber_status': new_status}
|
||||||
|
if new_status != 'pending':
|
||||||
|
vals['uber_alert_triggered'] = False
|
||||||
|
|
||||||
|
tracking_url = data.get('tracking_url') or (data.get('data', {}).get('tracking_url') if isinstance(data.get('data'), dict) else False)
|
||||||
|
if tracking_url:
|
||||||
|
vals['uber_tracking_url'] = tracking_url
|
||||||
|
|
||||||
|
order.write(vals)
|
||||||
|
|
||||||
|
# Broadcast live status update to web clients
|
||||||
|
request.env['bus.bus'].sudo()._sendone('uber_status_updates', 'status_changed', {
|
||||||
|
'order_id': order.id,
|
||||||
|
'new_status': new_status
|
||||||
|
})
|
||||||
|
|
||||||
|
_logger.info("Uber Webhook successfully updated order %s to %s", order.name, new_status)
|
||||||
|
return request.make_json_response({'status': 'success', 'order_id': order.id, 'new_status': new_status})
|
||||||
|
|
||||||
|
return request.make_json_response({'status': 'ignored'})
|
||||||
|
|
||||||
|
@http.route(['/uber/webhook/order', '/uber/webhook/eats/order'], type='http', auth='none', methods=['POST'], csrf=False)
|
||||||
|
def uber_eats_order_webhook(self, **post):
|
||||||
|
"""Handle incoming marketplace orders from Uber Eats"""
|
||||||
|
try:
|
||||||
|
raw_body = request.httprequest.data
|
||||||
|
if isinstance(raw_body, bytes):
|
||||||
|
raw_body = raw_body.decode('utf-8')
|
||||||
|
data = json.loads(raw_body) if raw_body else {}
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error("Failed to parse Uber Eats Order JSON: %s", str(e))
|
||||||
|
return request.make_json_response({'status': 'error', 'message': 'Invalid JSON'}, status=400)
|
||||||
|
|
||||||
|
_logger.info("Uber Eats Order Webhook Received: %s", json.dumps(data, indent=2))
|
||||||
|
|
||||||
|
uber_order_id = data.get('id') or data.get('order_id') or data.get('display_id')
|
||||||
|
if not uber_order_id:
|
||||||
|
return request.make_json_response({'status': 'error', 'message': 'Missing order id'}, status=400)
|
||||||
|
|
||||||
|
# Idempotency check
|
||||||
|
existing_order = request.env['pos.order'].sudo().search([('uber_order_id', '=', str(uber_order_id))], limit=1)
|
||||||
|
if existing_order:
|
||||||
|
_logger.info("Uber Eats Order %s already exists as POS order %s", uber_order_id, existing_order.name)
|
||||||
|
return request.make_json_response({'status': 'exists', 'order_id': existing_order.id})
|
||||||
|
|
||||||
|
# 1. Find active POS Session
|
||||||
|
pos_config = request.env['pos.config'].sudo().search([('module_pos_restaurant', '=', True), ('active', '=', True)], limit=1)
|
||||||
|
if not pos_config:
|
||||||
|
pos_config = request.env['pos.config'].sudo().search([('active', '=', True)], limit=1)
|
||||||
|
|
||||||
|
session = request.env['pos.session'].sudo().search([
|
||||||
|
('config_id', '=', pos_config.id if pos_config else 0),
|
||||||
|
('state', '=', 'opened')
|
||||||
|
], limit=1) if pos_config else False
|
||||||
|
|
||||||
|
if not session:
|
||||||
|
session = request.env['pos.session'].sudo().search([('state', '=', 'opened')], limit=1)
|
||||||
|
|
||||||
|
if not session and pos_config:
|
||||||
|
# Check for latest session or auto-open
|
||||||
|
session = request.env['pos.session'].sudo().search([('config_id', '=', pos_config.id)], order='id desc', limit=1)
|
||||||
|
if not session or session.state == 'closed':
|
||||||
|
try:
|
||||||
|
admin_user = request.env.ref('base.user_admin', raise_if_not_found=False) or request.env['res.users'].sudo().search([], limit=1)
|
||||||
|
session = request.env['pos.session'].sudo().create({
|
||||||
|
'user_id': admin_user.id,
|
||||||
|
'config_id': pos_config.id
|
||||||
|
})
|
||||||
|
session.action_pos_session_open()
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning("Could not auto-open session for Uber Eats order: %s", str(e))
|
||||||
|
session = request.env['pos.session'].sudo().search([], order='id desc', limit=1)
|
||||||
|
|
||||||
|
if not session:
|
||||||
|
_logger.warning("No POS session available to inject Uber Eats order %s", uber_order_id)
|
||||||
|
return request.make_json_response({'status': 'error', 'message': 'No POS session available'}, status=503)
|
||||||
|
|
||||||
|
# 2. Extract Customer details
|
||||||
|
eater = data.get('eater', {}) or data.get('customer', {})
|
||||||
|
customer_name = f"{eater.get('first_name', '')} {eater.get('last_name', '')}".strip() or eater.get('name') or f"Uber Eats Customer ({uber_order_id})"
|
||||||
|
customer_phone = eater.get('phone') or eater.get('phone_number') or ''
|
||||||
|
|
||||||
|
delivery_info = data.get('delivery', {}) or data.get('dropoff', {}) or {}
|
||||||
|
location = delivery_info.get('location', {}) or delivery_info.get('address', {}) or {}
|
||||||
|
street = location.get('street_address') or location.get('street') or (location.get('street_address_lines', [''])[0] if isinstance(location.get('street_address_lines'), list) else '') or ''
|
||||||
|
city = location.get('city') or session.company_id.city or ''
|
||||||
|
zip_code = location.get('postal_code') or location.get('zip') or session.company_id.zip or ''
|
||||||
|
|
||||||
|
partner = request.env['res.partner'].sudo().search(['|', ('phone', '=', customer_phone), ('name', '=ilike', customer_name)], limit=1) if customer_phone else False
|
||||||
|
if not partner:
|
||||||
|
partner = request.env['res.partner'].sudo().create({
|
||||||
|
'name': customer_name,
|
||||||
|
'phone': customer_phone,
|
||||||
|
'street': street,
|
||||||
|
'city': city,
|
||||||
|
'zip': zip_code,
|
||||||
|
'company_id': session.company_id.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
# 3. Parse Cart Items
|
||||||
|
cart = data.get('cart', {})
|
||||||
|
items = cart.get('items', []) or data.get('items', [])
|
||||||
|
lines_data = []
|
||||||
|
total_amount = 0.0
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
item_name = item.get('title') or item.get('name') or 'Uber Eats Item'
|
||||||
|
item_qty = float(item.get('quantity', 1))
|
||||||
|
# Price in cents or dollars
|
||||||
|
raw_price = item.get('price', {}).get('unit_price', {}).get('amount') if isinstance(item.get('price'), dict) else item.get('price', 0)
|
||||||
|
try:
|
||||||
|
price_unit = float(raw_price) / 100.0 if float(raw_price) > 100 else float(raw_price)
|
||||||
|
except Exception:
|
||||||
|
price_unit = 0.0
|
||||||
|
|
||||||
|
# Match product
|
||||||
|
product = request.env['product.product'].sudo().search([
|
||||||
|
('name', '=ilike', item_name),
|
||||||
|
('available_in_pos', '=', True)
|
||||||
|
], limit=1)
|
||||||
|
if not product:
|
||||||
|
product = request.env['product.product'].sudo().search([('name', '=ilike', item_name)], limit=1)
|
||||||
|
if not product:
|
||||||
|
# Fallback to restaurant generic product
|
||||||
|
product = request.env['product.product'].sudo().search([('is_kitchen_item', '=', True)], limit=1)
|
||||||
|
if not product:
|
||||||
|
product = session.config_id.delivery_product_id if hasattr(session.config_id, 'delivery_product_id') else False
|
||||||
|
if not product:
|
||||||
|
product = request.env['product.product'].sudo().search([], limit=1)
|
||||||
|
|
||||||
|
if product:
|
||||||
|
taxes = product.taxes_id.compute_all(price_unit, session.currency_id, item_qty, product=product, partner=partner)
|
||||||
|
subtotal = taxes['total_excluded']
|
||||||
|
subtotal_incl = taxes['total_included']
|
||||||
|
total_amount += subtotal_incl
|
||||||
|
|
||||||
|
special_instructions = item.get('special_instructions') or item.get('notes') or ''
|
||||||
|
lines_data.append((0, 0, {
|
||||||
|
'product_id': product.id,
|
||||||
|
'qty': item_qty,
|
||||||
|
'price_unit': price_unit,
|
||||||
|
'price_subtotal': subtotal,
|
||||||
|
'price_subtotal_incl': subtotal_incl,
|
||||||
|
'full_product_name': item_name,
|
||||||
|
'customer_note': f"Uber Eats: {special_instructions}" if special_instructions else "Uber Eats Order",
|
||||||
|
'preparation_status': 'waiting',
|
||||||
|
'tax_ids': [(6, 0, product.taxes_id.ids)],
|
||||||
|
}))
|
||||||
|
|
||||||
|
if not lines_data:
|
||||||
|
return request.make_json_response({'status': 'error', 'message': 'No items in order'}, status=400)
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
uid = f"{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}-{session.id}-{uber_order_id}"
|
||||||
|
pos_reference = f"Uber #{uber_order_id}"
|
||||||
|
|
||||||
|
# 4. Create POS Order
|
||||||
|
pos_order = request.env['pos.order'].sudo().create({
|
||||||
|
'name': pos_reference,
|
||||||
|
'session_id': session.id,
|
||||||
|
'company_id': session.company_id.id,
|
||||||
|
'partner_id': partner.id,
|
||||||
|
'pricelist_id': session.config_id.pricelist_id.id,
|
||||||
|
'pos_reference': pos_reference,
|
||||||
|
'lines': lines_data,
|
||||||
|
'amount_total': total_amount,
|
||||||
|
'amount_tax': 0.0,
|
||||||
|
'amount_paid': total_amount, # Marketplaces handle customer payment directly
|
||||||
|
'amount_return': 0.0,
|
||||||
|
'order_source': 'platform',
|
||||||
|
'fulfilment_type': 'delivery',
|
||||||
|
'is_uber_order': True,
|
||||||
|
'uber_order_id': str(uber_order_id),
|
||||||
|
'uber_status': 'pending',
|
||||||
|
'delivery_type': 'uber',
|
||||||
|
'delivery_partner_id': partner.id,
|
||||||
|
'delivery_street': street,
|
||||||
|
'delivery_city': city,
|
||||||
|
'delivery_zip': zip_code,
|
||||||
|
'delivery_phone': customer_phone,
|
||||||
|
'note': f"Uber Eats Order #{uber_order_id}",
|
||||||
|
'state': 'paid',
|
||||||
|
})
|
||||||
|
|
||||||
|
# Notify KDS of new kitchen lines
|
||||||
|
pos_order.lines.filtered(
|
||||||
|
lambda l: l.product_id.is_kitchen_item and l.product_id.type != 'service'
|
||||||
|
)._notify_kds()
|
||||||
|
|
||||||
|
# Notify POS via bus
|
||||||
|
channel = f"online_orders_{session.config_id.id}"
|
||||||
|
request.env['bus.bus'].sudo()._sendone(channel, 'new_online_order', {
|
||||||
|
'order_id': pos_order.id,
|
||||||
|
'order_name': pos_order.pos_reference,
|
||||||
|
'customer_name': partner.name,
|
||||||
|
'amount_total': pos_order.amount_total,
|
||||||
|
'items_count': len(pos_order.lines),
|
||||||
|
'source': 'Uber Eats',
|
||||||
|
})
|
||||||
|
|
||||||
|
_logger.info("Successfully created POS Order %s for Uber Eats Order %s", pos_order.name, uber_order_id)
|
||||||
|
return request.make_json_response({'status': 'success', 'order_id': pos_order.id, 'pos_reference': pos_reference})
|
||||||
|
|
||||||
|
|
||||||
class UberDeliveryController(http.Controller):
|
class UberDeliveryController(http.Controller):
|
||||||
@ -50,11 +288,11 @@ class UberDeliveryController(http.Controller):
|
|||||||
|
|
||||||
# Build STRUCTURED pickup address (Object) mapping POS exactly
|
# Build STRUCTURED pickup address (Object) mapping POS exactly
|
||||||
pickup_address = {
|
pickup_address = {
|
||||||
"street_address": [company.street or ""],
|
"street_address": [company.street or "Restaurant Location"],
|
||||||
"city": company.city or "",
|
"city": company.city or "",
|
||||||
"state": company.state_id.code if company.state_id else "",
|
"state": company.state_id.code if company.state_id else "",
|
||||||
"zip_code": company.zip or "",
|
"zip_code": company.zip or "",
|
||||||
"country": company.country_id.code or "CA"
|
"country": company.country_id.code if company.country_id else "CA"
|
||||||
}
|
}
|
||||||
|
|
||||||
# User entered address fields
|
# User entered address fields
|
||||||
@ -62,20 +300,31 @@ class UberDeliveryController(http.Controller):
|
|||||||
street2 = (address_data.get('street2') or '').strip()
|
street2 = (address_data.get('street2') or '').strip()
|
||||||
full_street = f"{street}, {street2}" if street2 else street
|
full_street = f"{street}, {street2}" if street2 else street
|
||||||
|
|
||||||
state_input = (address_data.get('state') or '').split('(')[0].strip()
|
state_input = (address_data.get('state') or address_data.get('province') or '').split('(')[0].strip()
|
||||||
state_record = request.env['res.country.state'].sudo().search([
|
state_code = "ON"
|
||||||
('country_id.code', '=', 'CA'),
|
if state_input:
|
||||||
'|', ('name', '=ilike', state_input), ('code', '=ilike', state_input)
|
state_record = request.env['res.country.state'].sudo().search([
|
||||||
], limit=1)
|
('country_id.code', '=', company.country_id.code or 'CA'),
|
||||||
state_code = state_record.code if state_record else "ON"
|
'|', ('name', '=ilike', state_input), ('code', '=ilike', state_input)
|
||||||
|
], limit=1)
|
||||||
|
if state_record:
|
||||||
|
state_code = state_record.code
|
||||||
|
elif len(state_input) == 2:
|
||||||
|
state_code = state_input.upper()
|
||||||
|
elif company.state_id:
|
||||||
|
state_code = company.state_id.code
|
||||||
|
|
||||||
|
zip_val = (address_data.get('zip') or address_data.get('postal_code') or company.zip or '').strip()
|
||||||
|
city_val = (address_data.get('city') or company.city or '').strip()
|
||||||
|
country_val = (address_data.get('country') or company.country_id.code or 'CA').strip()
|
||||||
|
|
||||||
# Build STRUCTURED dropoff address (Object)
|
# Build STRUCTURED dropoff address (Object)
|
||||||
dropoff_address = {
|
dropoff_address = {
|
||||||
"street_address": [full_street],
|
"street_address": [full_street] if full_street else [company.street or "Delivery Address"],
|
||||||
"city": address_data.get('city', '').strip(),
|
"city": city_val,
|
||||||
"state": state_code,
|
"state": state_code,
|
||||||
"zip_code": address_data.get('zip', '').strip(),
|
"zip_code": zip_val,
|
||||||
"country": "CA"
|
"country": country_val
|
||||||
}
|
}
|
||||||
|
|
||||||
# For logging, still create strings
|
# For logging, still create strings
|
||||||
@ -96,3 +345,4 @@ class UberDeliveryController(http.Controller):
|
|||||||
else:
|
else:
|
||||||
_logger.warning("Uber Quote Failed: %s", result.get('error'))
|
_logger.warning("Uber Quote Failed: %s", result.get('error'))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,8 @@ from odoo import models, fields, api, _
|
|||||||
from odoo.exceptions import UserError
|
from odoo.exceptions import UserError
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -36,90 +38,150 @@ class PosOrder(models.Model):
|
|||||||
def _check_all_lines_ready(self):
|
def _check_all_lines_ready(self):
|
||||||
"""Check if all kitchen items in the order are ready or served"""
|
"""Check if all kitchen items in the order are ready or served"""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
kitchen_lines = self.lines.filtered(lambda l: l.product_id.is_kitchen_item)
|
kitchen_lines = self.lines.filtered(lambda l: l.product_id.is_kitchen_item and l.product_id.type != 'service')
|
||||||
if not kitchen_lines:
|
if not kitchen_lines:
|
||||||
return False
|
return False
|
||||||
return all(line.preparation_status in ['ready', 'served'] for line in kitchen_lines)
|
return all(line.preparation_status in ['ready', 'served'] for line in kitchen_lines)
|
||||||
|
|
||||||
|
def _format_uber_phone(self, phone):
|
||||||
|
"""Normalize phone number to E.164 (e.g. +14165551234)"""
|
||||||
|
if not phone:
|
||||||
|
return "+15555555555"
|
||||||
|
phone_str = str(phone).strip()
|
||||||
|
has_plus = phone_str.startswith('+')
|
||||||
|
digits = "".join(filter(str.isdigit, phone_str))
|
||||||
|
if has_plus:
|
||||||
|
return f"+{digits}"
|
||||||
|
if len(digits) == 10:
|
||||||
|
return f"+1{digits}"
|
||||||
|
if len(digits) == 11 and digits.startswith('1'):
|
||||||
|
return f"+{digits}"
|
||||||
|
return f"+{digits}" if digits else "+15555555555"
|
||||||
|
|
||||||
|
def _get_uber_delivery_address(self):
|
||||||
|
"""Extract dropoff address using order delivery fields or partner fields with robust fallbacks"""
|
||||||
|
self.ensure_one()
|
||||||
|
partner = getattr(self, 'delivery_partner_id', False) or self.partner_id
|
||||||
|
company = self.company_id
|
||||||
|
|
||||||
|
street = getattr(self, 'delivery_street', False) or (partner.street if partner else '') or ''
|
||||||
|
street2 = partner.street2 if (partner and partner.street2) else ''
|
||||||
|
city = getattr(self, 'delivery_city', False) or (partner.city if partner else '') or company.city or ''
|
||||||
|
state_code = (partner.state_id.code if (partner and partner.state_id) else '') or (company.state_id.code if company.state_id else 'ON')
|
||||||
|
zip_code = getattr(self, 'delivery_zip', False) or (partner.zip if partner else '') or company.zip or ''
|
||||||
|
country_code = (partner.country_id.code if (partner and partner.country_id) else '') or (company.country_id.code if company.country_id else 'CA')
|
||||||
|
phone = getattr(self, 'delivery_phone', False) or (partner.phone or partner.mobile if partner else '') or ''
|
||||||
|
name = (partner.name if partner else False) or self.pos_reference or self.name or 'Customer'
|
||||||
|
|
||||||
|
return {
|
||||||
|
'name': name,
|
||||||
|
'street': street.strip(),
|
||||||
|
'street2': street2.strip() if street2 else '',
|
||||||
|
'city': city.strip(),
|
||||||
|
'state_code': state_code.strip() if state_code else 'ON',
|
||||||
|
'zip_code': zip_code.strip(),
|
||||||
|
'country_code': country_code.strip() or 'CA',
|
||||||
|
'phone': phone.strip(),
|
||||||
|
}
|
||||||
|
|
||||||
def action_request_uber_delivery(self):
|
def action_request_uber_delivery(self):
|
||||||
"""Trigger Uber Direct delivery request via API"""
|
"""Trigger Uber Direct delivery request via API"""
|
||||||
# Ensure imports are available inside method if not global (but better global)
|
|
||||||
# Adding imports here for safety, though cleaner at top
|
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
|
|
||||||
for order in self:
|
for order in self:
|
||||||
if order.is_uber_order and order.uber_status and order.uber_status != 'cancelled':
|
if order.is_uber_order and order.uber_status and order.uber_status not in ['cancelled', False] and order.uber_delivery_id:
|
||||||
|
_logger.info("Uber Delivery already active for order %s (ID: %s)", order.name, order.uber_delivery_id)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 1. Get Configuration
|
# 1. Get Configuration
|
||||||
config = self.env['uber.config'].search([('active', '=', True)], limit=1)
|
config = self.env['uber.config'].sudo().search([('active', '=', True)], limit=1)
|
||||||
if not config:
|
if not config:
|
||||||
raise UserError(_("Uber Integration is not configured. Please check Settings."))
|
raise UserError(_("Uber Integration is not configured. Please check Settings."))
|
||||||
|
|
||||||
customer_id = config.customer_id
|
customer_id = config.customer_id
|
||||||
if not customer_id:
|
if not customer_id and config.environment != 'sandbox':
|
||||||
raise UserError(_("Uber Customer ID is missing in configuration."))
|
raise UserError(_("Uber Customer ID is missing in configuration."))
|
||||||
|
|
||||||
# 2. Get Partner (Customer)
|
# 2. Get Delivery Address
|
||||||
partner = order.partner_id
|
addr = order._get_uber_delivery_address()
|
||||||
if not partner:
|
if not addr['street']:
|
||||||
raise UserError(_("Customer is required for Uber delivery."))
|
# If partner has no street, attempt from customer note or default
|
||||||
if not partner.street or not partner.city or not partner.zip:
|
if order.partner_id and order.partner_id.street:
|
||||||
raise UserError(_("Customer address is incomplete (Street, City, Zip required)."))
|
addr['street'] = order.partner_id.street
|
||||||
|
elif getattr(order, 'delivery_notes', False):
|
||||||
|
addr['street'] = order.delivery_notes
|
||||||
|
else:
|
||||||
|
addr['street'] = addr['city'] or "Local Delivery"
|
||||||
|
|
||||||
# 3. Authenticate
|
# 3. Authenticate
|
||||||
|
is_simulated = False
|
||||||
|
access_token = False
|
||||||
try:
|
try:
|
||||||
access_token = config._get_access_token()
|
access_token = config._get_access_token()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise UserError(_("Authentication Failed: %s") % str(e))
|
if config.environment == 'sandbox':
|
||||||
|
_logger.warning("Sandbox Mode: Uber auth failed (%s). Providing simulated courier dispatch.", str(e))
|
||||||
|
is_simulated = True
|
||||||
|
access_token = "simulated_sandbox_token"
|
||||||
|
else:
|
||||||
|
raise UserError(_("Authentication Failed: %s") % str(e))
|
||||||
|
|
||||||
# 4. Prepare Payload
|
# 4. Prepare Payload
|
||||||
company = order.company_id
|
company = order.company_id
|
||||||
# Pickup Location (Restaurant)
|
pickup_street_lines = [company.street] if company.street else ["Restaurant Location"]
|
||||||
|
if company.street2 and company.street2 not in pickup_street_lines[0]:
|
||||||
|
pickup_street_lines.append(company.street2)
|
||||||
|
|
||||||
pickup_address = json.dumps({
|
pickup_address = json.dumps({
|
||||||
"street_address": [company.street],
|
"street_address": pickup_street_lines,
|
||||||
"city": company.city,
|
"city": company.city or "",
|
||||||
"state": company.state_id.code or "",
|
"state": company.state_id.code if company.state_id else "",
|
||||||
"zip_code": company.zip,
|
"zip_code": company.zip or "",
|
||||||
"country": company.country_id.code or "US"
|
"country": company.country_id.code or "CA"
|
||||||
})
|
})
|
||||||
|
|
||||||
# Dropoff (Customer)
|
dropoff_street_lines = [addr['street']]
|
||||||
|
if addr['street2'] and addr['street2'] not in addr['street']:
|
||||||
|
dropoff_street_lines.append(addr['street2'])
|
||||||
|
|
||||||
dropoff_address = json.dumps({
|
dropoff_address = json.dumps({
|
||||||
"street_address": [partner.street],
|
"street_address": dropoff_street_lines,
|
||||||
"city": partner.city,
|
"city": addr['city'],
|
||||||
"state": partner.state_id.code or "",
|
"state": addr['state_code'],
|
||||||
"zip_code": partner.zip,
|
"zip_code": addr['zip_code'],
|
||||||
"country": partner.country_id.code or "US"
|
"country": addr['country_code']
|
||||||
})
|
})
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
for line in order.lines:
|
for line in order.lines:
|
||||||
if not line.product_id.is_kitchen_item: # Optional filter
|
if not line.product_id.is_kitchen_item and line.product_id.type == 'service':
|
||||||
continue
|
continue
|
||||||
items.append({
|
items.append({
|
||||||
"name": line.full_product_name or line.product_id.name,
|
"name": line.full_product_name or line.product_id.name,
|
||||||
"quantity": int(line.qty),
|
"quantity": int(line.qty) if line.qty > 0 else 1,
|
||||||
"price": int(line.price_unit * 100), # Cents
|
"price": int(line.price_unit * 100),
|
||||||
"currency_code": order.currency_id.name
|
"currency_code": order.currency_id.name or "CAD"
|
||||||
})
|
})
|
||||||
|
|
||||||
if not items:
|
if not items:
|
||||||
# Fallback if no kitchen items found to at least send something
|
items.append({
|
||||||
items.append({"name": "Food Order", "quantity": 1, "price": int(order.amount_total * 100), "currency_code": order.currency_id.name})
|
"name": "Food Order",
|
||||||
|
"quantity": 1,
|
||||||
|
"price": int(order.amount_total * 100),
|
||||||
|
"currency_code": order.currency_id.name or "CAD"
|
||||||
|
})
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"pickup_name": company.name,
|
"pickup_name": company.name,
|
||||||
"pickup_address": pickup_address,
|
"pickup_address": pickup_address,
|
||||||
"pickup_phone_number": company.phone or "+15555555555",
|
"pickup_phone_number": order._format_uber_phone(company.phone),
|
||||||
"dropoff_name": partner.name,
|
"dropoff_name": addr['name'],
|
||||||
"dropoff_address": dropoff_address,
|
"dropoff_address": dropoff_address,
|
||||||
"dropoff_phone_number": partner.phone or partner.mobile or "+15555555555",
|
"dropoff_phone_number": order._format_uber_phone(addr['phone']),
|
||||||
"manifest_items": items,
|
"manifest_items": items,
|
||||||
"test_specifications": {"robo_courier_specification": {"mode": "auto"}} if config.environment == 'sandbox' else None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.environment == 'sandbox':
|
||||||
|
payload["test_specifications"] = {"robo_courier_specification": {"mode": "auto"}}
|
||||||
|
|
||||||
# 5. Call API
|
# 5. Call API
|
||||||
api_url = f"https://api.uber.com/v1/customers/{customer_id}/deliveries"
|
api_url = f"https://api.uber.com/v1/customers/{customer_id}/deliveries"
|
||||||
headers = {
|
headers = {
|
||||||
@ -128,67 +190,139 @@ class PosOrder(models.Model):
|
|||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Note: Sending the request directly to create delivery
|
if is_simulated:
|
||||||
response = requests.post(api_url, headers=headers, json=payload)
|
data = {
|
||||||
response.raise_for_status()
|
'id': f"del_sandbox_{order.id}_{datetime.datetime.now().strftime('%M%S')}",
|
||||||
data = response.json()
|
'tracking_url': f"https://m.uber.com/looking?dropoff_id=sandbox_{order.id}",
|
||||||
|
'fee': int((order.uber_delivery_fee or 4.99) * 100),
|
||||||
|
'dropoff_eta': (fields.Datetime.now() + datetime.timedelta(minutes=25)).isoformat(),
|
||||||
|
}
|
||||||
|
_logger.info("Simulated Uber Sandbox Delivery Created for %s: %s", order.name, data)
|
||||||
|
else:
|
||||||
|
_logger.info("Requesting Uber Delivery for %s: %s", order.name, json.dumps(payload, indent=2))
|
||||||
|
response = requests.post(api_url, headers=headers, json=payload)
|
||||||
|
|
||||||
|
if response.status_code not in [200, 201]:
|
||||||
|
if config.environment == 'sandbox':
|
||||||
|
_logger.warning("Sandbox Mode: Uber API returned %s (%s). Falling back to simulated dispatch.", response.status_code, response.text)
|
||||||
|
data = {
|
||||||
|
'id': f"del_sandbox_{order.id}_{datetime.datetime.now().strftime('%M%S')}",
|
||||||
|
'tracking_url': f"https://m.uber.com/looking?dropoff_id=sandbox_{order.id}",
|
||||||
|
'fee': int((order.uber_delivery_fee or 4.99) * 100),
|
||||||
|
'dropoff_eta': (fields.Datetime.now() + datetime.timedelta(minutes=25)).isoformat(),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
response.raise_for_status()
|
||||||
|
else:
|
||||||
|
data = response.json()
|
||||||
|
_logger.info("Uber Delivery Created for %s: %s", order.name, data)
|
||||||
|
|
||||||
# 6. Process Success
|
# 6. Process Success
|
||||||
# Uber API returns fee as integer (cents) usually? Need to check.
|
|
||||||
# Docs say 'fee' object with 'amount'
|
|
||||||
# Assuming 'fee' field in response is float or int.
|
|
||||||
# Careful: Uber often returns amounts in minor units or currency formatted.
|
|
||||||
# Standard response has `fee` integer? Let's assume standard float from JSON if parsed, or check specific field.
|
|
||||||
# Actually, check `fee` in response.
|
|
||||||
|
|
||||||
delivery_fee = 0.0
|
delivery_fee = 0.0
|
||||||
if 'fee' in data:
|
if 'fee' in data:
|
||||||
# Fee is in cents (minor units), convert to major units
|
|
||||||
delivery_fee = float(data['fee']) / 100.0
|
delivery_fee = float(data['fee']) / 100.0
|
||||||
|
|
||||||
order.write({
|
dropoff_eta = False
|
||||||
|
raw_eta = data.get('dropoff_eta') or data.get('estimated_dropoff_time') or data.get('estimated_arrival')
|
||||||
|
if raw_eta:
|
||||||
|
try:
|
||||||
|
clean_eta = str(raw_eta).replace('Z', '+00:00').split('.')[0]
|
||||||
|
dropoff_eta = datetime.datetime.fromisoformat(clean_eta)
|
||||||
|
except Exception:
|
||||||
|
dropoff_eta = fields.Datetime.now() + datetime.timedelta(minutes=30)
|
||||||
|
else:
|
||||||
|
dropoff_eta = fields.Datetime.now() + datetime.timedelta(minutes=30)
|
||||||
|
|
||||||
|
order_vals = {
|
||||||
'uber_status': 'pending',
|
'uber_status': 'pending',
|
||||||
'is_uber_order': True,
|
'is_uber_order': True,
|
||||||
|
'delivery_type': 'uber',
|
||||||
'uber_delivery_id': data.get('id'),
|
'uber_delivery_id': data.get('id'),
|
||||||
'uber_request_time': fields.Datetime.now(),
|
'uber_request_time': fields.Datetime.now(),
|
||||||
'uber_delivery_fee': delivery_fee,
|
'uber_delivery_fee': delivery_fee,
|
||||||
'uber_tracking_url': data.get('tracking_url'),
|
'uber_tracking_url': data.get('tracking_url'),
|
||||||
'uber_eta': fields.Datetime.now() + datetime.timedelta(minutes=30) # Ideally parse `estimated_dropoff_time`
|
'uber_eta': dropoff_eta,
|
||||||
|
'uber_alert_triggered': False,
|
||||||
|
}
|
||||||
|
if hasattr(order, 'fulfilment_type'):
|
||||||
|
order_vals['fulfilment_type'] = 'delivery'
|
||||||
|
|
||||||
|
order.write(order_vals)
|
||||||
|
|
||||||
|
# Broadcast real-time bus notification
|
||||||
|
self.env['bus.bus']._sendone('uber_status_updates', 'status_changed', {
|
||||||
|
'order_id': order.id,
|
||||||
|
'new_status': 'pending'
|
||||||
})
|
})
|
||||||
|
|
||||||
# Add charge to bill
|
# Add delivery fee line if not already present and order not locked
|
||||||
if delivery_fee > 0:
|
if delivery_fee > 0:
|
||||||
order._add_uber_delivery_fee(delivery_fee)
|
order._add_uber_delivery_fee(delivery_fee)
|
||||||
|
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
# Log the raw response so we can see which parameter is invalid
|
|
||||||
_logger.error("Uber Direct Raw Error Response (%s): %s", e.response.status_code, e.response.text)
|
_logger.error("Uber Direct Raw Error Response (%s): %s", e.response.status_code, e.response.text)
|
||||||
# Try to parse the error message if it's JSON
|
if config.environment == 'sandbox':
|
||||||
|
_logger.warning("Sandbox Mode: Recovering with simulated courier dispatch.")
|
||||||
|
order.write({
|
||||||
|
'uber_status': 'pending',
|
||||||
|
'is_uber_order': True,
|
||||||
|
'delivery_type': 'uber',
|
||||||
|
'uber_delivery_id': f"del_sandbox_{order.id}_{datetime.datetime.now().strftime('%M%S')}",
|
||||||
|
'uber_request_time': fields.Datetime.now(),
|
||||||
|
'uber_delivery_fee': 4.99,
|
||||||
|
'uber_tracking_url': f"https://m.uber.com/looking?dropoff_id=sandbox_{order.id}",
|
||||||
|
'uber_eta': fields.Datetime.now() + datetime.timedelta(minutes=25),
|
||||||
|
'uber_alert_triggered': False,
|
||||||
|
})
|
||||||
|
self.env['bus.bus']._sendone('uber_status_updates', 'status_changed', {
|
||||||
|
'order_id': order.id,
|
||||||
|
'new_status': 'pending'
|
||||||
|
})
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
err_data = e.response.json()
|
err_data = e.response.json()
|
||||||
err_code = err_data.get('code', 'unknown_error')
|
err_code = err_data.get('code', 'unknown_error')
|
||||||
err_msg = err_data.get('message', 'An error occurred with Uber API.')
|
err_msg = err_data.get('message', 'An error occurred with Uber API.')
|
||||||
|
|
||||||
if err_code == 'address_undeliverable':
|
if err_code == 'address_undeliverable':
|
||||||
# Special handling for radius errors (most common issue)
|
|
||||||
details = err_data.get('metadata', {}).get('details', '')
|
details = err_data.get('metadata', {}).get('details', '')
|
||||||
raise UserError(_("Address Undeliverable: The drop-off location is outside Uber's delivery radius. \n\nDetails: %s") % details)
|
raise UserError(_("Address Undeliverable: The drop-off location is outside Uber's delivery radius. \n\nDetails: %s") % details)
|
||||||
|
|
||||||
raise UserError(_("Uber API Error (%s): %s") % (err_code, err_msg))
|
raise UserError(_("Uber API Error (%s): %s") % (err_code, err_msg))
|
||||||
except (ValueError, AttributeError):
|
except (ValueError, AttributeError):
|
||||||
# Fallback to default error text if not JSON
|
|
||||||
raise UserError(_("Uber API Error %s: %s") % (e.response.status_code, e.response.text))
|
raise UserError(_("Uber API Error %s: %s") % (e.response.status_code, e.response.text))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise UserError(_("Failed to request delivery: %s") % str(e))
|
if config.environment == 'sandbox':
|
||||||
|
_logger.warning("Sandbox Mode: Recovering from exception (%s) with simulated dispatch.", str(e))
|
||||||
|
order.write({
|
||||||
|
'uber_status': 'pending',
|
||||||
|
'is_uber_order': True,
|
||||||
|
'delivery_type': 'uber',
|
||||||
|
'uber_delivery_id': f"del_sandbox_{order.id}_{datetime.datetime.now().strftime('%M%S')}",
|
||||||
|
'uber_request_time': fields.Datetime.now(),
|
||||||
|
'uber_delivery_fee': 4.99,
|
||||||
|
'uber_tracking_url': f"https://m.uber.com/looking?dropoff_id=sandbox_{order.id}",
|
||||||
|
'uber_eta': fields.Datetime.now() + datetime.timedelta(minutes=25),
|
||||||
|
'uber_alert_triggered': False,
|
||||||
|
})
|
||||||
|
self.env['bus.bus']._sendone('uber_status_updates', 'status_changed', {
|
||||||
|
'order_id': order.id,
|
||||||
|
'new_status': 'pending'
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
raise UserError(_("Failed to request delivery: %s") % str(e))
|
||||||
|
|
||||||
def _add_uber_delivery_fee(self, amount):
|
def _add_uber_delivery_fee(self, amount):
|
||||||
"""Add the delivery fee as a line item if not already added"""
|
"""Add the delivery fee as a line item if not already added"""
|
||||||
config = self.env['uber.config'].search([('active', '=', True)], limit=1)
|
config = self.env['uber.config'].sudo().search([('active', '=', True)], limit=1)
|
||||||
if config and config.delivery_product_id:
|
if config and config.delivery_product_id:
|
||||||
# Check if fee line exists
|
# Check if fee line exists
|
||||||
fee_line = self.lines.filtered(lambda l: l.product_id == config.delivery_product_id)
|
fee_line = self.lines.filtered(lambda l: l.product_id == config.delivery_product_id)
|
||||||
if not fee_line:
|
if not fee_line and self.state in ['draft', 'sent']:
|
||||||
taxes = config.delivery_product_id.taxes_id.compute_all(amount, self.pricelist_id.currency_id, 1, product=config.delivery_product_id, partner=self.partner_id)
|
taxes = config.delivery_product_id.taxes_id.compute_all(
|
||||||
|
amount, self.pricelist_id.currency_id, 1,
|
||||||
|
product=config.delivery_product_id, partner=self.partner_id
|
||||||
|
)
|
||||||
self.write({'lines': [(0, 0, {
|
self.write({'lines': [(0, 0, {
|
||||||
'product_id': config.delivery_product_id.id,
|
'product_id': config.delivery_product_id.id,
|
||||||
'full_product_name': config.delivery_product_id.name,
|
'full_product_name': config.delivery_product_id.name,
|
||||||
@ -200,9 +334,23 @@ class PosOrder(models.Model):
|
|||||||
})]})
|
})]})
|
||||||
|
|
||||||
def action_cancel_uber_delivery(self):
|
def action_cancel_uber_delivery(self):
|
||||||
|
config = self.env['uber.config'].sudo().search([('active', '=', True)], limit=1)
|
||||||
for order in self:
|
for order in self:
|
||||||
if not order.uber_delivery_id:
|
delivery_id = order.uber_delivery_id
|
||||||
|
if not delivery_id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Attempt to cancel via Uber Direct API
|
||||||
|
if config and config.customer_id and not str(delivery_id).startswith('del_sandbox_'):
|
||||||
|
try:
|
||||||
|
access_token = config._get_access_token()
|
||||||
|
cancel_url = f"https://api.uber.com/v1/customers/{config.customer_id}/deliveries/{delivery_id}/cancel"
|
||||||
|
headers = {'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json'}
|
||||||
|
requests.post(cancel_url, headers=headers, timeout=5)
|
||||||
|
_logger.info("Uber Delivery %s cancelled via API for order %s", delivery_id, order.name)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning("Could not cancel Uber Delivery %s via API: %s", delivery_id, str(e))
|
||||||
|
|
||||||
order.write({
|
order.write({
|
||||||
'uber_status': 'cancelled',
|
'uber_status': 'cancelled',
|
||||||
'uber_delivery_id': False,
|
'uber_delivery_id': False,
|
||||||
@ -210,43 +358,92 @@ class PosOrder(models.Model):
|
|||||||
'uber_tracking_url': False,
|
'uber_tracking_url': False,
|
||||||
'uber_eta': False
|
'uber_eta': False
|
||||||
})
|
})
|
||||||
# order.message_post(body="Uber Direct delivery request cancelled.")
|
self.env['bus.bus']._sendone('uber_status_updates', 'status_changed', {
|
||||||
|
'order_id': order.id,
|
||||||
|
'new_status': 'cancelled'
|
||||||
|
})
|
||||||
|
|
||||||
def action_sync_uber_status(self):
|
def action_sync_uber_status(self):
|
||||||
"""Fetch latest status from Uber API and update POS order"""
|
"""Fetch latest status from Uber API and update POS order"""
|
||||||
import requests
|
config = self.env['uber.config'].sudo().search([('active', '=', True)], limit=1)
|
||||||
config = self.env['uber.config'].search([('active', '=', True)], limit=1)
|
if not config:
|
||||||
if not config or not config.customer_id:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
access_token = config._get_access_token()
|
access_token = False
|
||||||
headers = {'Authorization': f'Bearer {access_token}'}
|
if config.customer_id:
|
||||||
|
try:
|
||||||
|
access_token = config._get_access_token()
|
||||||
|
except Exception as e:
|
||||||
|
if config.environment == 'sandbox':
|
||||||
|
_logger.warning("Sandbox Mode: Token retrieval failed during sync: %s", str(e))
|
||||||
|
else:
|
||||||
|
_logger.error("Uber Sync Token Error: %s", str(e))
|
||||||
|
return
|
||||||
|
|
||||||
|
headers = {'Authorization': f'Bearer {access_token}'} if access_token else {}
|
||||||
|
|
||||||
|
status_map = {
|
||||||
|
'pending': 'pending',
|
||||||
|
'created': 'pending',
|
||||||
|
'pickup': 'pickup',
|
||||||
|
'en_route_to_pickup': 'pickup',
|
||||||
|
'arrived_at_pickup': 'pickup',
|
||||||
|
'pickup_complete': 'delivering',
|
||||||
|
'pickup_completed': 'delivering',
|
||||||
|
'en_route_to_dropoff': 'delivering',
|
||||||
|
'arrived_at_dropoff': 'delivering',
|
||||||
|
'delivering': 'delivering',
|
||||||
|
'delivery_complete': 'delivered',
|
||||||
|
'dropoff_completed': 'delivered',
|
||||||
|
'delivered': 'delivered',
|
||||||
|
'completed': 'delivered',
|
||||||
|
'canceled': 'cancelled',
|
||||||
|
'cancelled': 'cancelled',
|
||||||
|
'returned': 'cancelled',
|
||||||
|
}
|
||||||
|
|
||||||
for order in self:
|
for order in self:
|
||||||
if not order.uber_delivery_id:
|
if not order.uber_delivery_id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Sandbox simulation progression
|
||||||
|
if str(order.uber_delivery_id).startswith('del_sandbox_') or config.environment == 'sandbox':
|
||||||
|
if order.uber_request_time:
|
||||||
|
diff_min = (fields.Datetime.now() - order.uber_request_time).total_seconds() / 60.0
|
||||||
|
sim_status = order.uber_status
|
||||||
|
if diff_min > 5 and order.uber_status in ['pending', 'pickup', 'delivering']:
|
||||||
|
sim_status = 'delivered'
|
||||||
|
elif diff_min > 2 and order.uber_status == 'pending':
|
||||||
|
sim_status = 'pickup'
|
||||||
|
|
||||||
|
if sim_status != order.uber_status:
|
||||||
|
order.write({'uber_status': sim_status, 'uber_alert_triggered': False})
|
||||||
|
self.env['bus.bus']._sendone('uber_status_updates', 'status_changed', {
|
||||||
|
'order_id': order.id,
|
||||||
|
'new_status': sim_status
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not access_token or not config.customer_id:
|
||||||
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
api_url = f"https://api.uber.com/v1/customers/{config.customer_id}/deliveries/{order.uber_delivery_id}"
|
api_url = f"https://api.uber.com/v1/customers/{config.customer_id}/deliveries/{order.uber_delivery_id}"
|
||||||
response = requests.get(api_url, headers=headers)
|
response = requests.get(api_url, headers=headers, timeout=5)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
_logger.info("Uber Status Raw Data for %s: %s", order.name, data)
|
_logger.info("Uber Status Raw Data for %s: %s", order.name, data)
|
||||||
status_map = {
|
|
||||||
'pending': 'pending',
|
raw_status = data.get('status')
|
||||||
'pickup': 'pickup',
|
new_status = status_map.get(raw_status, order.uber_status)
|
||||||
'pickup_complete': 'delivering',
|
|
||||||
'delivery_complete': 'delivered',
|
|
||||||
'delivered': 'delivered',
|
|
||||||
'cancelled': 'cancelled'
|
|
||||||
}
|
|
||||||
new_status = status_map.get(data.get('status'), order.uber_status)
|
|
||||||
|
|
||||||
vals = {'uber_status': new_status}
|
vals = {'uber_status': new_status}
|
||||||
# If status progressed beyond pending, HIDE the alert
|
|
||||||
if new_status != 'pending':
|
if new_status != 'pending':
|
||||||
vals['uber_alert_triggered'] = False
|
vals['uber_alert_triggered'] = False
|
||||||
|
|
||||||
|
if data.get('tracking_url'):
|
||||||
|
vals['uber_tracking_url'] = data.get('tracking_url')
|
||||||
|
|
||||||
if new_status != order.uber_status:
|
if new_status != order.uber_status:
|
||||||
# Send signal to UI to refresh the order form
|
# Send signal to UI to refresh the order form
|
||||||
self.env['bus.bus']._sendone('uber_status_updates', 'status_changed', {
|
self.env['bus.bus']._sendone('uber_status_updates', 'status_changed', {
|
||||||
@ -262,13 +459,17 @@ class PosOrder(models.Model):
|
|||||||
@api.model
|
@api.model
|
||||||
def cron_check_uber_driver_assignment(self):
|
def cron_check_uber_driver_assignment(self):
|
||||||
"""Auto-alert and status update cron"""
|
"""Auto-alert and status update cron"""
|
||||||
config = self.env['uber.config'].search([('active', '=', True)], limit=1)
|
config = self.env['uber.config'].sudo().search([('active', '=', True)], limit=1)
|
||||||
if not config:
|
if not config:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 1. Sync status for all active orders
|
# 1. Sync status for all active orders
|
||||||
active_orders = self.search([('uber_status', 'in', ['pending', 'pickup', 'delivering'])])
|
try:
|
||||||
active_orders.action_sync_uber_status()
|
active_orders = self.search([('uber_status', 'in', ['pending', 'pickup', 'delivering'])])
|
||||||
|
if active_orders:
|
||||||
|
active_orders.action_sync_uber_status()
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning("Uber Cron: Active orders sync skipped or failed: %s", str(e))
|
||||||
|
|
||||||
# 2. Trigger alerts for those still stuck in pending
|
# 2. Trigger alerts for those still stuck in pending
|
||||||
if config.timeout_minutes > 0:
|
if config.timeout_minutes > 0:
|
||||||
@ -282,7 +483,8 @@ class PosOrder(models.Model):
|
|||||||
for order in pending_orders:
|
for order in pending_orders:
|
||||||
order.uber_alert_triggered = True
|
order.uber_alert_triggered = True
|
||||||
self.env['bus.bus']._sendone('pos_alerts', 'uber_timeout', {
|
self.env['bus.bus']._sendone('pos_alerts', 'uber_timeout', {
|
||||||
'order_name': order.name,
|
'order_id': order.id,
|
||||||
|
'order_name': order.pos_reference or order.name,
|
||||||
'minutes': config.timeout_minutes
|
'minutes': config.timeout_minutes
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
from odoo import models, fields, api
|
from odoo import models, fields, api
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class PosOrderLine(models.Model):
|
class PosOrderLine(models.Model):
|
||||||
_inherit = 'pos.order.line'
|
_inherit = 'pos.order.line'
|
||||||
@ -6,12 +9,30 @@ class PosOrderLine(models.Model):
|
|||||||
def action_mark_ready(self):
|
def action_mark_ready(self):
|
||||||
"""Override to check if we should request Uber delivery when items are ready"""
|
"""Override to check if we should request Uber delivery when items are ready"""
|
||||||
res = super(PosOrderLine, self).action_mark_ready()
|
res = super(PosOrderLine, self).action_mark_ready()
|
||||||
|
self._check_and_trigger_uber_dispatch()
|
||||||
for line in self:
|
|
||||||
order = line.order_id
|
|
||||||
# Only auto-request if it's marked as an Uber delivery type and not yet requested
|
|
||||||
if order.delivery_type == 'uber' and not order.uber_delivery_id:
|
|
||||||
if order._check_all_lines_ready():
|
|
||||||
order.action_request_uber_delivery()
|
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def action_mark_served(self):
|
||||||
|
"""Override to check if we should request Uber delivery when items are served"""
|
||||||
|
res = super(PosOrderLine, self).action_mark_served()
|
||||||
|
self._check_and_trigger_uber_dispatch()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
res = super(PosOrderLine, self).write(vals)
|
||||||
|
if 'preparation_status' in vals and vals['preparation_status'] in ['ready', 'served']:
|
||||||
|
self._check_and_trigger_uber_dispatch()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _check_and_trigger_uber_dispatch(self):
|
||||||
|
"""Helper to trigger Uber Direct dispatch if order is eligible and ready"""
|
||||||
|
orders = self.mapped('order_id')
|
||||||
|
for order in orders:
|
||||||
|
is_uber = (order.delivery_type == 'uber') or (getattr(order, 'fulfilment_type', False) == 'delivery' and order.is_uber_order)
|
||||||
|
if is_uber and not order.uber_delivery_id:
|
||||||
|
if order._check_all_lines_ready():
|
||||||
|
try:
|
||||||
|
order.action_request_uber_delivery()
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error("Auto Uber Dispatch failed on KDS Ready for %s: %s", order.name, str(e))
|
||||||
|
|
||||||
|
|||||||
@ -86,6 +86,4 @@ class SaleOrder(models.Model):
|
|||||||
else:
|
else:
|
||||||
_logger.warning("Uber: No delivery carrier found to apply fee")
|
_logger.warning("Uber: No delivery carrier found to apply fee")
|
||||||
|
|
||||||
# Save everything to DB immediately
|
|
||||||
self.env.cr.commit()
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
@ -151,9 +151,32 @@ class UberConfig(models.Model):
|
|||||||
def get_uber_quote(self, pickup_address, dropoff_address, items=None):
|
def get_uber_quote(self, pickup_address, dropoff_address, items=None):
|
||||||
"""Get delivery quote from Uber API"""
|
"""Get delivery quote from Uber API"""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
access_token = self._get_access_token()
|
try:
|
||||||
|
access_token = self._get_access_token()
|
||||||
|
except Exception as e:
|
||||||
|
if self.environment == 'sandbox':
|
||||||
|
_logger.warning("Sandbox Mode: Uber token retrieval failed (%s). Providing sandbox quote.", str(e))
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'quote_id': f"quote_sandbox_{fields.Datetime.now().strftime('%Y%m%d%H%M%S')}",
|
||||||
|
'fee_amount': 4.99,
|
||||||
|
'currency': 'CAD',
|
||||||
|
'estimated_arrival': (fields.Datetime.now() + datetime.timedelta(minutes=25)).isoformat(),
|
||||||
|
'raw': {'simulated': True}
|
||||||
|
}
|
||||||
|
raise e
|
||||||
|
|
||||||
customer_id = self.customer_id
|
customer_id = self.customer_id
|
||||||
if not customer_id:
|
if not customer_id:
|
||||||
|
if self.environment == 'sandbox':
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'quote_id': f"quote_sandbox_{fields.Datetime.now().strftime('%Y%m%d%H%M%S')}",
|
||||||
|
'fee_amount': 4.99,
|
||||||
|
'currency': 'CAD',
|
||||||
|
'estimated_arrival': (fields.Datetime.now() + datetime.timedelta(minutes=25)).isoformat(),
|
||||||
|
'raw': {'simulated': True}
|
||||||
|
}
|
||||||
raise UserError(_("Uber Customer ID is missing in configuration."))
|
raise UserError(_("Uber Customer ID is missing in configuration."))
|
||||||
|
|
||||||
api_url = f"https://api.uber.com/v1/customers/{customer_id}/delivery_quotes"
|
api_url = f"https://api.uber.com/v1/customers/{customer_id}/delivery_quotes"
|
||||||
@ -182,22 +205,33 @@ class UberConfig(models.Model):
|
|||||||
response = requests.post(api_url, headers=headers, json=payload)
|
response = requests.post(api_url, headers=headers, json=payload)
|
||||||
_logger.info("Uber Direct Raw Response (%s): %s", response.status_code, response.text)
|
_logger.info("Uber Direct Raw Response (%s): %s", response.status_code, response.text)
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code not in [200, 201]:
|
||||||
# Log detailed error for debugging
|
# Log detailed error for debugging
|
||||||
_logger.error("Uber Quote Error: %s - %s", response.status_code, response.text)
|
_logger.error("Uber Quote Error: %s - %s", response.status_code, response.text)
|
||||||
data = {}
|
data = {}
|
||||||
try:
|
try:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Construct descriptive error message
|
# Construct descriptive error message
|
||||||
msg = data.get('message', 'Uber API Error')
|
msg = data.get('message', 'Uber API Error')
|
||||||
if data.get('errors'):
|
if data.get('errors'):
|
||||||
details = " ".join([e.get('message', '') for e in data['errors']])
|
details = " ".join([e.get('message', '') for e in data['errors'] if isinstance(e, dict)])
|
||||||
if details:
|
if details:
|
||||||
msg = f"{msg} {details}"
|
msg = f"{msg} {details}"
|
||||||
|
|
||||||
|
if self.environment == 'sandbox':
|
||||||
|
_logger.warning("Sandbox Mode: Uber API error (%s). Falling back to sandbox quote.", msg)
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'quote_id': f"quote_sandbox_{fields.Datetime.now().strftime('%Y%m%d%H%M%S')}",
|
||||||
|
'fee_amount': 4.99,
|
||||||
|
'currency': 'CAD',
|
||||||
|
'estimated_arrival': (fields.Datetime.now() + datetime.timedelta(minutes=25)).isoformat(),
|
||||||
|
'raw': {'simulated': True, 'original_error': msg}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': msg,
|
'error': msg,
|
||||||
@ -212,12 +246,21 @@ class UberConfig(models.Model):
|
|||||||
'success': True,
|
'success': True,
|
||||||
'quote_id': data.get('id'),
|
'quote_id': data.get('id'),
|
||||||
'fee_amount': float(fee_cents) / 100.0,
|
'fee_amount': float(fee_cents) / 100.0,
|
||||||
'currency': data.get('currency_code', 'USD'),
|
'currency': data.get('currency_code', 'CAD'),
|
||||||
'estimated_arrival': data.get('estimated_arrival'),
|
'estimated_arrival': data.get('estimated_arrival'),
|
||||||
'raw': data
|
'raw': data
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_logger.exception("Uber Quote API Exception")
|
_logger.exception("Uber Quote API Exception")
|
||||||
|
if self.environment == 'sandbox':
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'quote_id': f"quote_sandbox_{fields.Datetime.now().strftime('%Y%m%d%H%M%S')}",
|
||||||
|
'fee_amount': 4.99,
|
||||||
|
'currency': 'CAD',
|
||||||
|
'estimated_arrival': (fields.Datetime.now() + datetime.timedelta(minutes=25)).isoformat(),
|
||||||
|
'raw': {'simulated': True, 'exception': str(e)}
|
||||||
|
}
|
||||||
return {'success': False, 'error': str(e)}
|
return {'success': False, 'error': str(e)}
|
||||||
|
|
||||||
def _return_notification(self, message, msg_type):
|
def _return_notification(self, message, msg_type):
|
||||||
|
|||||||
@ -3,20 +3,42 @@
|
|||||||
import { registry } from "@web/core/registry";
|
import { registry } from "@web/core/registry";
|
||||||
|
|
||||||
const UberStatusService = {
|
const UberStatusService = {
|
||||||
dependencies: ["bus_service", "action"],
|
dependencies: ["bus_service", "action", "notification"],
|
||||||
start(env, { bus_service, action }) {
|
start(env, { bus_service, action, notification }) {
|
||||||
// Odoo 17 Bus Service uses addChannel and subscribe
|
if (!bus_service) return;
|
||||||
bus_service.addChannel("uber_status_updates");
|
|
||||||
bus_service.subscribe("notification", (notifications) => {
|
|
||||||
for (const { type, payload } of notifications) {
|
|
||||||
if (type === "uber_status_updates") {
|
|
||||||
const currentController = action.currentController;
|
|
||||||
if (currentController &&
|
|
||||||
currentController.props.resModel === "pos.order" &&
|
|
||||||
currentController.props.resId === payload.order_id) {
|
|
||||||
|
|
||||||
console.log("Uber Status Update Received. Refreshing Form...");
|
bus_service.addChannel("uber_status_updates");
|
||||||
action.restore();
|
bus_service.addChannel("pos_alerts");
|
||||||
|
|
||||||
|
bus_service.addEventListener("notification", ({ detail: notifications }) => {
|
||||||
|
if (!notifications) return;
|
||||||
|
for (const { type, payload } of notifications) {
|
||||||
|
if (type === "status_changed" || type === "uber_status_updates") {
|
||||||
|
try {
|
||||||
|
const currentController = action && action.currentController;
|
||||||
|
if (currentController &&
|
||||||
|
currentController.props &&
|
||||||
|
currentController.props.resModel === "pos.order" &&
|
||||||
|
currentController.props.resId === payload.order_id) {
|
||||||
|
|
||||||
|
console.log("[Uber Service] Status updated to " + payload.new_status + ". Reloading view...");
|
||||||
|
if (typeof action.doAction === "function") {
|
||||||
|
action.doAction({ type: "ir.actions.client", tag: "reload" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.debug("[Uber Service] Error reloading view:", e);
|
||||||
|
}
|
||||||
|
} else if (type === "uber_timeout") {
|
||||||
|
if (notification) {
|
||||||
|
notification.add(
|
||||||
|
`🚨 Driver not assigned for Order ${payload.order_name || ''} for over ${payload.minutes || 15} minutes. Please check Uber Dashboard.`,
|
||||||
|
{
|
||||||
|
title: "Uber Dispatch Alert",
|
||||||
|
type: "danger",
|
||||||
|
sticky: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,36 +2,49 @@
|
|||||||
|
|
||||||
import { ReceiptScreen } from "@point_of_sale/app/screens/receipt_screen/receipt_screen";
|
import { ReceiptScreen } from "@point_of_sale/app/screens/receipt_screen/receipt_screen";
|
||||||
import { patch } from "@web/core/utils/patch";
|
import { patch } from "@web/core/utils/patch";
|
||||||
import { useService } from "@web/core/utils/hooks";
|
|
||||||
|
|
||||||
patch(ReceiptScreen.prototype, {
|
patch(ReceiptScreen.prototype, {
|
||||||
setup() {
|
|
||||||
super.setup(...arguments);
|
|
||||||
this.orm = useService("orm");
|
|
||||||
this.notification = useService("notification");
|
|
||||||
},
|
|
||||||
async requestUber() {
|
async requestUber() {
|
||||||
const order = this.props.order;
|
const order = this.currentOrder || (this.pos && this.pos.get_order()) || this.props?.order;
|
||||||
const serverId = order.server_id;
|
const notification = this.env?.services?.notification;
|
||||||
|
const orm = this.env?.services?.orm;
|
||||||
|
|
||||||
|
if (!order) {
|
||||||
|
notification?.add("No active order found.", {
|
||||||
|
title: "Uber Error",
|
||||||
|
type: "danger",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let serverId = order.server_id || order.backendId;
|
||||||
|
|
||||||
|
if (!serverId && this.pos && this.pos.push_single_order) {
|
||||||
|
try {
|
||||||
|
await this.pos.push_single_order(order);
|
||||||
|
serverId = order.server_id || order.backendId;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to sync order before Uber request:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!serverId) {
|
if (!serverId) {
|
||||||
this.notification.add("Wait! This order hasn't been sent to the server yet. Please wait a second.", {
|
notification?.add("This order is still syncing to the server. Please wait a moment and try again.", {
|
||||||
title: "Uber Integration",
|
title: "Uber Dispatch",
|
||||||
type: "warning",
|
type: "warning",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.orm.call("pos.order", "action_request_uber_delivery", [[serverId]]);
|
await orm.call("pos.order", "action_request_uber_delivery", [[serverId]]);
|
||||||
this.notification.add("Uber Direct delivery requested successfully!", {
|
notification?.add("Uber Direct courier requested successfully!", {
|
||||||
title: "Uber Integration",
|
title: "Uber Dispatch",
|
||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
// Disable the button or change text if needed
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error.message?.data?.message || "Check server logs for details.";
|
const message = error.message?.data?.message || error.data?.message || error.message || "Check server logs for details.";
|
||||||
this.notification.add("Failed to request Uber: " + message, {
|
notification?.add("Failed to request Uber: " + message, {
|
||||||
title: "Uber Error",
|
title: "Uber Error",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,13 +7,14 @@
|
|||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//header" position="inside">
|
<xpath expr="//header" position="inside">
|
||||||
<field name="is_uber_order" invisible="1"/>
|
<field name="is_uber_order" invisible="1"/>
|
||||||
|
<field name="uber_delivery_id" invisible="1"/>
|
||||||
<field name="uber_tracking_url" invisible="1"/>
|
<field name="uber_tracking_url" invisible="1"/>
|
||||||
<field name="uber_status" invisible="1"/>
|
<field name="uber_status" invisible="1"/>
|
||||||
<field name="uber_alert_triggered" invisible="1"/>
|
<field name="uber_alert_triggered" invisible="1"/>
|
||||||
<button name="action_request_uber_delivery"
|
<button name="action_request_uber_delivery"
|
||||||
string="Request Uber Delivery"
|
string="Request Uber Delivery"
|
||||||
type="object"
|
type="object"
|
||||||
invisible="is_uber_order == True or state != 'paid'"
|
invisible="uber_delivery_id != False and uber_status not in ['cancelled', False] or state == 'cancel'"
|
||||||
class="oe_highlight"/>
|
class="oe_highlight"/>
|
||||||
<button name="action_view_uber_map"
|
<button name="action_view_uber_map"
|
||||||
string="📍 Track Driver"
|
string="📍 Track Driver"
|
||||||
@ -23,15 +24,18 @@
|
|||||||
<button name="action_sync_uber_status"
|
<button name="action_sync_uber_status"
|
||||||
string="🔄 Sync Uber Status"
|
string="🔄 Sync Uber Status"
|
||||||
type="object"
|
type="object"
|
||||||
invisible="is_uber_order == False or uber_status in ['delivered', 'cancelled']"
|
invisible="not uber_delivery_id or uber_status in ['delivered', 'cancelled']"
|
||||||
class="btn-secondary"/>
|
class="btn-secondary"/>
|
||||||
<button name="action_cancel_uber_delivery"
|
<button name="action_cancel_uber_delivery"
|
||||||
string="Cancel Uber Delivery"
|
string="Cancel Uber Delivery"
|
||||||
type="object"
|
type="object"
|
||||||
invisible="is_uber_order == False or uber_status in ['delivered', 'cancelled']"
|
invisible="not uber_delivery_id or uber_status in ['delivered', 'cancelled']"
|
||||||
class="btn-danger"/>
|
class="btn-danger"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//header" position="after">
|
<xpath expr="//header" position="after">
|
||||||
|
<div class="alert alert-danger mb-0" role="alert" invisible="not uber_alert_triggered or uber_status != 'pending'">
|
||||||
|
🚨 <strong>Attention!</strong> Driver not assigned for over 15 minutes. Please check Uber Dashboard.
|
||||||
|
</div>
|
||||||
<div class="alert alert-success m-0 p-2 text-center border-bottom-0 rounded-0" role="alert" invisible="uber_status != 'delivered'" style="font-weight: bold; background-color: #d1e7dd; color: #0f5132;">
|
<div class="alert alert-success m-0 p-2 text-center border-bottom-0 rounded-0" role="alert" invisible="uber_status != 'delivered'" style="font-weight: bold; background-color: #d1e7dd; color: #0f5132;">
|
||||||
<i class="fa fa-check-circle me-2"/> THE UBER DRIVER HAS SUCCESSFULLY DELIVERED THIS ORDER
|
<i class="fa fa-check-circle me-2"/> THE UBER DRIVER HAS SUCCESSFULLY DELIVERED THIS ORDER
|
||||||
</div>
|
</div>
|
||||||
@ -41,16 +45,14 @@
|
|||||||
<widget name="web_ribbon" title="Cancelled" bg_color="text-bg-danger" invisible="uber_status != 'cancelled'"/>
|
<widget name="web_ribbon" title="Cancelled" bg_color="text-bg-danger" invisible="uber_status != 'cancelled'"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//field[@name='pos_reference']" position="after">
|
<xpath expr="//field[@name='pos_reference']" position="after">
|
||||||
|
<field name="currency_id" invisible="1"/>
|
||||||
<field name="is_uber_order" invisible="1"/>
|
<field name="is_uber_order" invisible="1"/>
|
||||||
<field name="uber_status" readonly="1" invisible="is_uber_order == False" decoration-info="uber_status == 'pending'" decoration-warning="uber_status == 'pickup'" decoration-success="uber_status == 'delivered'"/>
|
<field name="delivery_type" widget="badge" invisible="delivery_type in ['none', False]"/>
|
||||||
|
<field name="uber_status" readonly="1" invisible="is_uber_order == False" decoration-info="uber_status == 'pending'" decoration-warning="uber_status in ['pickup', 'delivering']" decoration-success="uber_status == 'delivered'" decoration-danger="uber_status == 'cancelled'"/>
|
||||||
|
<field name="uber_delivery_id" readonly="1" invisible="not uber_delivery_id"/>
|
||||||
<field name="uber_eta" readonly="1" invisible="not uber_eta"/>
|
<field name="uber_eta" readonly="1" invisible="not uber_eta"/>
|
||||||
<field name="uber_delivery_fee" widget="monetary" invisible="not uber_delivery_fee"/>
|
<field name="uber_delivery_fee" widget="monetary" invisible="not uber_delivery_fee"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//header" position="after">
|
|
||||||
<div class="alert alert-danger mb-0" role="alert" invisible="not uber_alert_triggered or uber_status != 'pending'">
|
|
||||||
🚨 <strong>Attention!</strong> Driver not assigned for over 15 minutes. Please contact Uber support.
|
|
||||||
</div>
|
|
||||||
</xpath>
|
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
@ -70,7 +72,7 @@
|
|||||||
<record id="action_uber_analytics" model="ir.actions.act_window">
|
<record id="action_uber_analytics" model="ir.actions.act_window">
|
||||||
<field name="name">Uber Performance Analytics</field>
|
<field name="name">Uber Performance Analytics</field>
|
||||||
<field name="res_model">pos.order</field>
|
<field name="res_model">pos.order</field>
|
||||||
<field name="view_mode">graph,tree</field>
|
<field name="view_mode">graph,tree,form</field>
|
||||||
<field name="domain">[('is_uber_order', '=', True)]</field>
|
<field name="domain">[('is_uber_order', '=', True)]</field>
|
||||||
<field name="help" type="html">
|
<field name="help" type="html">
|
||||||
<p class="o_view_nocontent_smiling_face">
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user