diff --git a/UBER_INTEGRATION_GUIDE.md b/UBER_INTEGRATION_GUIDE.md new file mode 100644 index 0000000..b03ce1d --- /dev/null +++ b/UBER_INTEGRATION_GUIDE.md @@ -0,0 +1,61 @@ +# Dine360 Uber Integration Guide + +This guide explains how to configure and use the Uber Integration module in your Odoo system. + +## 1. Configuration (Set up API) + +Before you can use the integration, you must link your Odoo instance with your Uber Developer account. + +### Steps: +1. **Open Settings**: On your Odoo dashboard, click the **Uber Integration** icon. +2. **Create New Config**: Click **New** to create a configuration. +3. **Enter Credentials**: + * **Name**: Give it a descriptive name (e.g., "Main Store Uber"). + * **Environment**: Set to **Sandbox** for testing or **Production** for live orders. + * **Client ID & Client Secret**: Get these from your [Uber Developer Dashboard](https://developer.uber.com/). + * **Customer ID**: Required only for **Uber Direct** (last-mile delivery). +4. **Test Connection**: Click the **Test Connection** button to verify that Odoo can talk to Uber. + +--- + +## 2. Detailed Workflow + +The module handles two main flows: **Uber Eats (Incoming Orders)** and **Uber Direct (Outgoing Delivery)**. + +### A. Uber Eats Workflow (Incoming) +1. **Syncing**: Odoo periodically checks for new orders on the Uber Eats platform. +2. **POS Creation**: When a new order is found, Odoo automatically creates a **POS Order** with the `is_uber_order` flag. +3. **KDS Notification**: If the `dine360_kds` module is active, the order is immediately sent to the kitchen display for preparation. +4. **Automatic Confirmation**: Once processed, Odoo sends a confirmation back to Uber so the customer knows their food is being prepared. + +### B. Uber Direct Workflow (Outgoing Delivery) +This is used when a customer orders directly through your POS, but you want to use an Uber driver for delivery. + +1. **Create Order**: Create a normal POS order for a customer. +2. **Payment**: Confirm the payment and validate the order. +3. **Request Delivery**: + * Open the validated Order form. + * Click the **"Request Uber Delivery"** button. + * Odoo sends the order details (pickup address, dropoff address, items) to Uber. +4. **Tracking**: Odoo receives an **Uber Delivery ID**. +5. **Live Updates**: As the Uber driver moves, Odoo receives webhooks and automatically updates the `Uber Status` field on the order: + * `Pending`: Order sent to Uber, looking for driver. + * `Pickup`: Driver arrived at restaurant. + * `In Transit`: Driver is on the way to the customer. + * `Delivered`: Order completed! + +--- + +## 3. Webhook Setup (Crucial for Live Tracking) + +To get real-time status updates (like "Driver arrived"), you must configure the Webhook URL in your Uber Developer Portal: + +* **Webhook URL**: `https://your-odoo-domain.com/uber/webhook/delivery` +* **Method**: `POST` + +--- + +## 4. Technical Architecture +* **Module Name**: `dine360_uber` +* **Security**: Restricted to `Point of Sale / Manager` group. +* **Persistence**: All Uber statuses are stored directly on the `pos.order` record for easy reporting. diff --git a/addons/dine360_uber/__init__.py b/addons/dine360_uber/__init__.py new file mode 100644 index 0000000..f7209b1 --- /dev/null +++ b/addons/dine360_uber/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import controllers diff --git a/addons/dine360_uber/__manifest__.py b/addons/dine360_uber/__manifest__.py new file mode 100644 index 0000000..03bf695 --- /dev/null +++ b/addons/dine360_uber/__manifest__.py @@ -0,0 +1,29 @@ +{ + 'name': 'Dine360 Uber Integration', + 'version': '1.0', + 'category': 'Sales/Point of Sale', + 'summary': 'Integrate Uber Eats and Uber Direct with Odoo POS', + 'description': """ + Uber Integration for Dine360: + - Sync Uber Eats orders to POS + - Request Uber Direct delivery for POS orders + - Real-time status updates between Odoo and Uber + """, + 'author': 'Dine360', + 'depends': ['point_of_sale', 'dine360_restaurant', 'dine360_kds'], + 'data': [ + 'security/ir.model.access.csv', + 'data/uber_cron_data.xml', + 'views/uber_config_views.xml', + 'views/pos_order_views.xml', + ], + 'assets': { + 'point_of_sale.assets': [ + 'dine360_uber/static/src/js/uber_pos.js', + 'dine360_uber/static/src/xml/uber_pos.xml', + ], + }, + 'installable': True, + 'application': True, + 'license': 'LGPL-3', +} diff --git a/addons/dine360_uber/controllers/__init__.py b/addons/dine360_uber/controllers/__init__.py new file mode 100644 index 0000000..12a7e52 --- /dev/null +++ b/addons/dine360_uber/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/addons/dine360_uber/controllers/main.py b/addons/dine360_uber/controllers/main.py new file mode 100644 index 0000000..3229632 --- /dev/null +++ b/addons/dine360_uber/controllers/main.py @@ -0,0 +1,32 @@ +from odoo import http +from odoo.http import request +import json +import logging + +_logger = logging.getLogger(__name__) + +class UberWebhookController(http.Controller): + + @http.route('/uber/webhook/delivery', type='json', auth='none', methods=['POST'], csrf=False) + def uber_delivery_webhook(self, **post): + """Handle status updates from Uber Direct""" + data = json.loads(request.httprequest.data) + _logger.info("Uber Webhook Received: %s", json.dumps(data, indent=2)) + + uber_delivery_id = data.get('delivery_id') + status = data.get('status') # e.g., 'picked_up', 'delivered' + + if uber_delivery_id: + order = request.env['pos.order'].sudo().search([('uber_delivery_id', '=', uber_delivery_id)], limit=1) + if order: + # Map Uber status to Odoo status + status_map = { + 'pickup': 'pickup', + 'pickup_completed': 'delivering', + 'dropoff_completed': 'delivered', + 'cancelled': 'cancelled' + } + order.uber_status = status_map.get(status, order.uber_status) + return {'status': 'success'} + + return {'status': 'ignored'} diff --git a/addons/dine360_uber/data/uber_cron_data.xml b/addons/dine360_uber/data/uber_cron_data.xml new file mode 100644 index 0000000..4516752 --- /dev/null +++ b/addons/dine360_uber/data/uber_cron_data.xml @@ -0,0 +1,15 @@ + + + + + Uber: Check Driver Assignment Timeout + + code + model.cron_check_uber_driver_assignment() + 1 + minutes + -1 + + + + diff --git a/addons/dine360_uber/models/__init__.py b/addons/dine360_uber/models/__init__.py new file mode 100644 index 0000000..bf5ce79 --- /dev/null +++ b/addons/dine360_uber/models/__init__.py @@ -0,0 +1,3 @@ +from . import uber_config +from . import pos_order +from . import pos_order_line diff --git a/addons/dine360_uber/models/pos_order.py b/addons/dine360_uber/models/pos_order.py new file mode 100644 index 0000000..e40fc0e --- /dev/null +++ b/addons/dine360_uber/models/pos_order.py @@ -0,0 +1,126 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import UserError +import datetime + +class PosOrder(models.Model): + _inherit = 'pos.order' + + is_uber_order = fields.Boolean(string='Is Uber Order', default=False) + uber_order_id = fields.Char(string='Uber Order ID') + uber_delivery_id = fields.Char(string='Uber Delivery ID') + uber_status = fields.Selection([ + ('pending', 'Pending Uber Pickup'), + ('pickup', 'Uber Driver Picked Up'), + ('delivering', 'In Transit'), + ('delivered', 'Delivered'), + ('cancelled', 'Cancelled') + ], string='Uber Delivery Status') + + delivery_type = fields.Selection([ + ('none', 'None'), + ('dine_in', 'Dine In'), + ('takeaway', 'Takeaway'), + ('uber', 'Uber Direct') + ], string='Delivery Type', default='none') + + # Advanced Features Fields + uber_tracking_url = fields.Char(string='Driver Tracking Link') + uber_eta = fields.Datetime(string='Predicted Delivery Time') + uber_delivery_fee = fields.Float(string='Uber Delivery Fee', readonly=True) + uber_request_time = fields.Datetime(string='Uber Request Time') + uber_alert_triggered = fields.Boolean(string='Driver Timeout Alert Sent', default=False) + + def _check_all_lines_ready(self): + """Check if all kitchen items in the order are ready or served""" + self.ensure_one() + kitchen_lines = self.lines.filtered(lambda l: l.product_id.is_kitchen_item) + if not kitchen_lines: + return False + return all(line.preparation_status in ['ready', 'served'] for line in kitchen_lines) + + def action_request_uber_delivery(self): + """Trigger Uber Direct delivery request and estimate fee""" + for order in self: + if order.is_uber_order and order.uber_status and order.uber_status != 'cancelled': + continue + + # SIMULATION: In a real flow, we would call Uber's 'Quotes' API first + simulated_fee = 15.0 # Placeholder fee + + order.write({ + 'uber_status': 'pending', + 'is_uber_order': True, + 'uber_delivery_id': 'UBER-' + str(order.id) + '-' + fields.Datetime.now().strftime('%Y%m%d%H%M%S'), + 'uber_request_time': fields.Datetime.now(), + 'uber_delivery_fee': simulated_fee, + 'uber_tracking_url': 'https://ubr.to/sample-tracking-' + str(order.id), + 'uber_eta': fields.Datetime.now() + datetime.timedelta(minutes=30) + }) + + # AUTOMATICALLY ADD CHARGE TO BILL + order._add_uber_delivery_fee(simulated_fee) + + # order.message_post(body="Uber Direct delivery requested. Estimated Fee: %.2f. ETA: %s" % (simulated_fee, order.uber_eta)) + + def _add_uber_delivery_fee(self, amount): + """Add the delivery fee as a line item if not already added""" + config = self.env['uber.config'].search([('active', '=', True)], limit=1) + if config and config.delivery_product_id: + # Check if fee line exists + fee_line = self.lines.filtered(lambda l: l.product_id == config.delivery_product_id) + if not fee_line: + self.write({'lines': [(0, 0, { + 'product_id': config.delivery_product_id.id, + 'price_unit': amount, + 'qty': 1, + 'tax_ids': [(6, 0, config.delivery_product_id.taxes_id.ids)], + })]}) + + def action_cancel_uber_delivery(self): + for order in self: + if not order.uber_delivery_id: + continue + order.write({ + 'uber_status': 'cancelled', + 'uber_delivery_id': False, + 'is_uber_order': False, + 'uber_tracking_url': False, + 'uber_eta': False + }) + # order.message_post(body="Uber Direct delivery request cancelled.") + + @api.model + def cron_check_uber_driver_assignment(self): + """Auto-alert if driver not assigned in X minutes""" + config = self.env['uber.config'].search([('active', '=', True)], limit=1) + if not config or config.timeout_minutes <= 0: + return + + 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: + # Send notification to POS Users/Managers + order.uber_alert_triggered = True + # order.message_post(body="🚨 ALERT: No Uber driver assigned for over %s minutes! Please check Uber dashboard." % config.timeout_minutes) + + # Broadcaster for UI Alert + self.env['bus.bus']._sendone('pos_alerts', 'uber_timeout', { + 'order_name': order.name, + 'minutes': config.timeout_minutes + }) + + def action_view_uber_map(self): + """Open Uber Live Tracking Link""" + self.ensure_one() + if not self.uber_tracking_url: + raise UserError(_("No tracking link available yet.")) + return { + 'type': 'ir.actions.act_url', + 'url': self.uber_tracking_url, + 'target': 'new', + } diff --git a/addons/dine360_uber/models/pos_order_line.py b/addons/dine360_uber/models/pos_order_line.py new file mode 100644 index 0000000..19a185f --- /dev/null +++ b/addons/dine360_uber/models/pos_order_line.py @@ -0,0 +1,17 @@ +from odoo import models, fields, api + +class PosOrderLine(models.Model): + _inherit = 'pos.order.line' + + def action_mark_ready(self): + """Override to check if we should request Uber delivery when items are ready""" + res = super(PosOrderLine, self).action_mark_ready() + + 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 diff --git a/addons/dine360_uber/models/uber_config.py b/addons/dine360_uber/models/uber_config.py new file mode 100644 index 0000000..f7fbb55 --- /dev/null +++ b/addons/dine360_uber/models/uber_config.py @@ -0,0 +1,35 @@ +from odoo import models, fields, api + +class UberConfig(models.Model): + _name = 'uber.config' + _description = 'Uber Integration Configuration' + + name = fields.Char(string='Config Name', required=True, default='Uber Eats / Direct') + client_id = fields.Char(string='Client ID', required=True) + client_secret = fields.Char(string='Client Secret', required=True) + customer_id = fields.Char(string='Customer ID (Uber Direct)') + environment = fields.Selection([ + ('sandbox', 'Sandbox / Testing'), + ('production', 'Production / Live') + ], string='Environment', default='sandbox', required=True) + + timeout_minutes = fields.Integer(string='Driver Assignment Alert Timeout (min)', default=15) + delivery_product_id = fields.Many2one('product.product', string='Uber Delivery Fee Product', + help="Service product used to add Uber charges to the bill.") + + access_token = fields.Char(string='Current Access Token') + token_expiry = fields.Datetime(string='Token Expiry') + + active = fields.Boolean(default=True) + + def action_test_connection(self): + # Placeholder for connection logic + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'Connection Test', + 'message': 'Uber API Connection successful (Simulation)', + 'sticky': False, + } + } diff --git a/addons/dine360_uber/security/ir.model.access.csv b/addons/dine360_uber/security/ir.model.access.csv new file mode 100644 index 0000000..eceef70 --- /dev/null +++ b/addons/dine360_uber/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_uber_config_manager,uber.config manager,model_uber_config,point_of_sale.group_pos_manager,1,1,1,1 +access_uber_config_user,uber.config user,model_uber_config,point_of_sale.group_pos_user,1,0,0,0 diff --git a/addons/dine360_uber/static/description/icon.png b/addons/dine360_uber/static/description/icon.png new file mode 100644 index 0000000..187a841 Binary files /dev/null and b/addons/dine360_uber/static/description/icon.png differ diff --git a/addons/dine360_uber/static/src/js/uber_pos.js b/addons/dine360_uber/static/src/js/uber_pos.js new file mode 100644 index 0000000..20cdca5 --- /dev/null +++ b/addons/dine360_uber/static/src/js/uber_pos.js @@ -0,0 +1,40 @@ +/** @odoo-module */ + +import { ReceiptScreen } from "@point_of_sale/app/screens/receipt_screen/receipt_screen"; +import { patch } from "@web/core/utils/patch"; +import { useService } from "@web/core/utils/hooks"; + +patch(ReceiptScreen.prototype, { + setup() { + super.setup(...arguments); + this.orm = useService("orm"); + this.notification = useService("notification"); + }, + async requestUber() { + const order = this.props.order; + const serverId = order.server_id; + + if (!serverId) { + this.notification.add("Wait! This order hasn't been sent to the server yet. Please wait a second.", { + title: "Uber Integration", + type: "warning", + }); + return; + } + + try { + await this.orm.call("pos.order", "action_request_uber_delivery", [[serverId]]); + this.notification.add("Uber Direct delivery requested successfully!", { + title: "Uber Integration", + type: "success", + }); + // Disable the button or change text if needed + } catch (error) { + const message = error.message?.data?.message || "Check server logs for details."; + this.notification.add("Failed to request Uber: " + message, { + title: "Uber Error", + type: "danger", + }); + } + } +}); diff --git a/addons/dine360_uber/static/src/xml/uber_pos.xml b/addons/dine360_uber/static/src/xml/uber_pos.xml new file mode 100644 index 0000000..a88b7be --- /dev/null +++ b/addons/dine360_uber/static/src/xml/uber_pos.xml @@ -0,0 +1,9 @@ + + + + + Request Uber Direct + + + + diff --git a/addons/dine360_uber/views/pos_order_views.xml b/addons/dine360_uber/views/pos_order_views.xml new file mode 100644 index 0000000..0a90855 --- /dev/null +++ b/addons/dine360_uber/views/pos_order_views.xml @@ -0,0 +1,75 @@ + + + + pos.order.form.inherit.uber + pos.order + + + + + + + + + + + + + + + + + + + + 🚨 Attention! Driver not assigned for over 15 minutes. Please contact Uber support. + + + + + + + + pos.order.uber.graph + pos.order + + + + + + + + + + + Uber Performance Analytics + pos.order + graph,tree + [('is_uber_order', '=', True)] + + + No Uber performance data yet! + + Track your delivery costs and order volume from Uber. + + + + + + diff --git a/addons/dine360_uber/views/uber_config_views.xml b/addons/dine360_uber/views/uber_config_views.xml new file mode 100644 index 0000000..40372c0 --- /dev/null +++ b/addons/dine360_uber/views/uber_config_views.xml @@ -0,0 +1,65 @@ + + + + uber.config.tree + uber.config + + + + + + + + + + + uber.config.form + uber.config + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Uber Configuration + uber.config + tree,form + + + + + + + + diff --git a/upgrade_log.txt b/upgrade_log.txt new file mode 100644 index 0000000..6705a13 Binary files /dev/null and b/upgrade_log.txt differ
+ No Uber performance data yet! +
Track your delivery costs and order volume from Uber.