forked from alaguraj/odoo-testing-addons
add Uber integration for POS/Sales orders and customize checkout address UI with delivery/pickup selection
This commit is contained in:
parent
3c6e33cc37
commit
4c80bdf027
@ -385,9 +385,10 @@
|
|||||||
|
|
||||||
const addressText = selectedCard.querySelector('address')?.innerText || "";
|
const addressText = selectedCard.querySelector('address')?.innerText || "";
|
||||||
const parts = addressText.split('\n').map(p => p.trim());
|
const parts = addressText.split('\n').map(p => p.trim());
|
||||||
// Crude parsing
|
|
||||||
const street = parts[1] || "";
|
const street = parts[1] || "";
|
||||||
const cityZip = parts[2] || "";
|
const zipMatch = addressText.match(/[A-Z][0-9][A-Z]\s?[0-9][A-Z][0-9]/);
|
||||||
|
const zip = zipMatch ? zipMatch[0] : "";
|
||||||
|
const city = parts[2] ? parts[2].split(' ')[0] : "";
|
||||||
|
|
||||||
msgBox.className = 'alert alert-info my-3';
|
msgBox.className = 'alert alert-info my-3';
|
||||||
msgBox.innerHTML = "Verifying Uber coverage for this address...";
|
msgBox.innerHTML = "Verifying Uber coverage for this address...";
|
||||||
@ -396,14 +397,15 @@
|
|||||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({ params: { address_data: {
|
body: JSON.stringify({ params: { address_data: {
|
||||||
street: street,
|
street: street,
|
||||||
zip: cityZip.split(' ').pop(),
|
zip: zip,
|
||||||
city: cityZip.split(' ')[0],
|
city: city,
|
||||||
} } })
|
} } })
|
||||||
}).then(r => r.json()).then(data => {
|
}).then(r => r.json()).then(data => {
|
||||||
if (data.result && data.result.success) {
|
if (data.result && data.result.success) {
|
||||||
msgBox.className = 'alert alert-success my-3';
|
msgBox.className = 'alert alert-success my-3';
|
||||||
msgBox.innerHTML = `<strong>✓ Uber Delivery Available!</strong> Fee: $${data.result.fee}`;
|
msgBox.innerHTML = `<strong>✓ Uber Delivery Available!</strong> Fee: $${data.result.fee}`;
|
||||||
document.querySelector('button[type="submit"]')?.removeAttribute('disabled');
|
document.querySelector('button[type="submit"]')?.removeAttribute('disabled');
|
||||||
|
setTimeout(() => { window.location.reload(); }, 1200);
|
||||||
} else {
|
} else {
|
||||||
msgBox.className = 'alert alert-danger my-3';
|
msgBox.className = 'alert alert-danger my-3';
|
||||||
msgBox.innerHTML = `<strong>✕ Uber Direct: Invalid Operation</strong><br/>${data.result?.error || "Outside delivery radius."}`;
|
msgBox.innerHTML = `<strong>✕ Uber Direct: Invalid Operation</strong><br/>${data.result?.error || "Outside delivery radius."}`;
|
||||||
@ -413,6 +415,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.querySelectorAll('input[name="partner_id"]').forEach(i => i.addEventListener('change', verifySelectedAddress));
|
document.querySelectorAll('input[name="partner_id"]').forEach(i => i.addEventListener('change', verifySelectedAddress));
|
||||||
|
|
||||||
|
// NEW: Ultimate trigger for every touch on an address box
|
||||||
|
$(document).on('click', '#address_selection .card, #address_selection label', function() {
|
||||||
|
setTimeout(verifySelectedAddress, 50); // Small delay to let radio button update
|
||||||
|
});
|
||||||
|
|
||||||
verifySelectedAddress();
|
verifySelectedAddress();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,9 @@
|
|||||||
'dine360_uber/static/src/js/uber_pos.js',
|
'dine360_uber/static/src/js/uber_pos.js',
|
||||||
'dine360_uber/static/src/xml/uber_pos.xml',
|
'dine360_uber/static/src/xml/uber_pos.xml',
|
||||||
],
|
],
|
||||||
|
'web.assets_backend': [
|
||||||
|
'dine360_uber/static/src/js/uber_backend.js',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
'installable': True,
|
'installable': True,
|
||||||
'application': True,
|
'application': True,
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
from odoo import models, fields, api, _
|
from odoo import models, fields, api, _
|
||||||
from odoo.exceptions import UserError
|
from odoo.exceptions import UserError
|
||||||
import datetime
|
import datetime
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class PosOrder(models.Model):
|
class PosOrder(models.Model):
|
||||||
_inherit = 'pos.order'
|
_inherit = 'pos.order'
|
||||||
@ -158,6 +161,8 @@ class PosOrder(models.Model):
|
|||||||
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)
|
||||||
# Try to parse the error message if it's JSON
|
# Try to parse the error message if it's JSON
|
||||||
try:
|
try:
|
||||||
err_data = e.response.json()
|
err_data = e.response.json()
|
||||||
@ -207,30 +212,79 @@ class PosOrder(models.Model):
|
|||||||
})
|
})
|
||||||
# order.message_post(body="Uber Direct delivery request cancelled.")
|
# order.message_post(body="Uber Direct delivery request cancelled.")
|
||||||
|
|
||||||
@api.model
|
def action_sync_uber_status(self):
|
||||||
def cron_check_uber_driver_assignment(self):
|
"""Fetch latest status from Uber API and update POS order"""
|
||||||
"""Auto-alert if driver not assigned in X minutes"""
|
import requests
|
||||||
config = self.env['uber.config'].search([('active', '=', True)], limit=1)
|
config = self.env['uber.config'].search([('active', '=', True)], limit=1)
|
||||||
if not config or config.timeout_minutes <= 0:
|
if not config or not config.customer_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
timeout_threshold = fields.Datetime.now() - datetime.timedelta(minutes=config.timeout_minutes)
|
access_token = config._get_access_token()
|
||||||
pending_orders = self.search([
|
headers = {'Authorization': f'Bearer {access_token}'}
|
||||||
('uber_status', '=', 'pending'),
|
|
||||||
('uber_request_time', '<=', timeout_threshold),
|
|
||||||
('uber_alert_triggered', '=', False)
|
|
||||||
])
|
|
||||||
|
|
||||||
for order in pending_orders:
|
for order in self:
|
||||||
# Send notification to POS Users/Managers
|
if not order.uber_delivery_id:
|
||||||
order.uber_alert_triggered = True
|
continue
|
||||||
# order.message_post(body="🚨 ALERT: No Uber driver assigned for over %s minutes! Please check Uber dashboard." % config.timeout_minutes)
|
|
||||||
|
|
||||||
# Broadcaster for UI Alert
|
try:
|
||||||
self.env['bus.bus']._sendone('pos_alerts', 'uber_timeout', {
|
api_url = f"https://api.uber.com/v1/customers/{config.customer_id}/deliveries/{order.uber_delivery_id}"
|
||||||
'order_name': order.name,
|
response = requests.get(api_url, headers=headers)
|
||||||
'minutes': config.timeout_minutes
|
if response.status_code == 200:
|
||||||
})
|
data = response.json()
|
||||||
|
_logger.info("Uber Status Raw Data for %s: %s", order.name, data)
|
||||||
|
status_map = {
|
||||||
|
'pending': 'pending',
|
||||||
|
'pickup': 'pickup',
|
||||||
|
'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}
|
||||||
|
# If status progressed beyond pending, HIDE the alert
|
||||||
|
if new_status != 'pending':
|
||||||
|
vals['uber_alert_triggered'] = False
|
||||||
|
|
||||||
|
if new_status != order.uber_status:
|
||||||
|
# Send signal to UI to refresh the order form
|
||||||
|
self.env['bus.bus']._sendone('uber_status_updates', 'status_changed', {
|
||||||
|
'order_id': order.id,
|
||||||
|
'new_status': new_status
|
||||||
|
})
|
||||||
|
|
||||||
|
order.write(vals)
|
||||||
|
_logger.info("Uber Status Synced for %s: %s", order.name, new_status)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error("Failed to sync Uber status for %s: %s", order.name, str(e))
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def cron_check_uber_driver_assignment(self):
|
||||||
|
"""Auto-alert and status update cron"""
|
||||||
|
config = self.env['uber.config'].search([('active', '=', True)], limit=1)
|
||||||
|
if not config:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 1. Sync status for all active orders
|
||||||
|
active_orders = self.search([('uber_status', 'in', ['pending', 'pickup', 'delivering'])])
|
||||||
|
active_orders.action_sync_uber_status()
|
||||||
|
|
||||||
|
# 2. Trigger alerts for those still stuck in pending
|
||||||
|
if config.timeout_minutes > 0:
|
||||||
|
timeout_threshold = fields.Datetime.now() - datetime.timedelta(minutes=config.timeout_minutes)
|
||||||
|
pending_orders = self.search([
|
||||||
|
('uber_status', '=', 'pending'),
|
||||||
|
('uber_request_time', '<=', timeout_threshold),
|
||||||
|
('uber_alert_triggered', '=', False)
|
||||||
|
])
|
||||||
|
|
||||||
|
for order in pending_orders:
|
||||||
|
order.uber_alert_triggered = True
|
||||||
|
self.env['bus.bus']._sendone('pos_alerts', 'uber_timeout', {
|
||||||
|
'order_name': order.name,
|
||||||
|
'minutes': config.timeout_minutes
|
||||||
|
})
|
||||||
|
|
||||||
def action_view_uber_map(self):
|
def action_view_uber_map(self):
|
||||||
"""Open Uber Live Tracking Link"""
|
"""Open Uber Live Tracking Link"""
|
||||||
|
|||||||
@ -23,15 +23,25 @@ class SaleOrder(models.Model):
|
|||||||
Carrier = self.env['delivery.carrier'].sudo()
|
Carrier = self.env['delivery.carrier'].sudo()
|
||||||
config = self.env['uber.config'].sudo().search([('active', '=', True)], limit=1)
|
config = self.env['uber.config'].sudo().search([('active', '=', True)], limit=1)
|
||||||
|
|
||||||
carrier = Carrier.search([('name', 'ilike', 'Uber')], limit=1)
|
# Search for any carrier linked to the Uber product or with Uber in the name
|
||||||
if not carrier and config and config.delivery_product_id:
|
carrier = Carrier.search(['|', ('name', 'ilike', 'Uber'), ('product_id', '=', config.delivery_product_id.id if config.delivery_product_id else 0)], limit=1)
|
||||||
_logger.info("Uber: Creating new Uber Delivery carrier")
|
|
||||||
|
if not carrier and config:
|
||||||
|
# Fallback product if one isn't set in config
|
||||||
|
product = config.delivery_product_id
|
||||||
|
if not product:
|
||||||
|
product = self.env['product.product'].sudo().search([('name', 'ilike', 'Delivery')], limit=1)
|
||||||
|
if not product:
|
||||||
|
product = self.env['product.product'].sudo().search([], limit=1) # Last resort
|
||||||
|
|
||||||
|
_logger.info("Uber: Creating new Uber Delivery carrier using product %s", product.name)
|
||||||
carrier = Carrier.create({
|
carrier = Carrier.create({
|
||||||
'name': 'Uber Delivery',
|
'name': 'Uber Direct Delivery',
|
||||||
'delivery_type': 'fixed',
|
'delivery_type': 'fixed',
|
||||||
'product_id': config.delivery_product_id.id,
|
'product_id': product.id,
|
||||||
'website_published': True,
|
'website_published': True,
|
||||||
'fixed_price': 0.0,
|
'fixed_price': 0.0,
|
||||||
|
'active': True,
|
||||||
})
|
})
|
||||||
|
|
||||||
if carrier:
|
if carrier:
|
||||||
|
|||||||
27
addons/dine360_uber/static/src/js/uber_backend.js
Normal file
27
addons/dine360_uber/static/src/js/uber_backend.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { registry } from "@web/core/registry";
|
||||||
|
|
||||||
|
const UberStatusService = {
|
||||||
|
dependencies: ["bus_service", "action"],
|
||||||
|
start(env, { bus_service, action }) {
|
||||||
|
// Odoo 17 Bus Service uses addChannel and subscribe
|
||||||
|
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...");
|
||||||
|
action.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.category("services").add("uber_status_service", UberStatusService);
|
||||||
@ -18,14 +18,28 @@
|
|||||||
<button name="action_view_uber_map"
|
<button name="action_view_uber_map"
|
||||||
string="📍 Track Driver"
|
string="📍 Track Driver"
|
||||||
type="object"
|
type="object"
|
||||||
invisible="not uber_tracking_url"
|
invisible="not uber_tracking_url or uber_status in ['delivered', 'cancelled']"
|
||||||
class="btn-primary"/>
|
class="btn-primary"/>
|
||||||
|
<button name="action_sync_uber_status"
|
||||||
|
string="🔄 Sync Uber Status"
|
||||||
|
type="object"
|
||||||
|
invisible="is_uber_order == False or uber_status in ['delivered', 'cancelled']"
|
||||||
|
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="is_uber_order == False or uber_status in ['delivered', 'cancelled']"
|
||||||
class="btn-danger"/>
|
class="btn-danger"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
|
<xpath expr="//header" position="after">
|
||||||
|
<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
|
||||||
|
</div>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//sheet" position="inside">
|
||||||
|
<widget name="web_ribbon" title="Delivered" bg_color="text-bg-success" invisible="uber_status != 'delivered'"/>
|
||||||
|
<widget name="web_ribbon" title="Cancelled" bg_color="text-bg-danger" invisible="uber_status != 'cancelled'"/>
|
||||||
|
</xpath>
|
||||||
<xpath expr="//field[@name='pos_reference']" position="after">
|
<xpath expr="//field[@name='pos_reference']" position="after">
|
||||||
<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="uber_status" readonly="1" invisible="is_uber_order == False" decoration-info="uber_status == 'pending'" decoration-warning="uber_status == 'pickup'" decoration-success="uber_status == 'delivered'"/>
|
||||||
@ -33,7 +47,7 @@
|
|||||||
<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">
|
<xpath expr="//header" position="after">
|
||||||
<div class="alert alert-danger mb-0" role="alert" invisible="not uber_alert_triggered">
|
<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.
|
🚨 <strong>Attention!</strong> Driver not assigned for over 15 minutes. Please contact Uber support.
|
||||||
</div>
|
</div>
|
||||||
</xpath>
|
</xpath>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user