From d31150ce4592ebe429a1200dafe62e25d98ddb8a Mon Sep 17 00:00:00 2001 From: Alaguraj0361 Date: Tue, 17 Feb 2026 21:35:02 +0530 Subject: [PATCH] Implement Uber integration module for POS, enabling Uber Eats order syncing and Uber Direct delivery management with webhook tracking. --- UBER_INTEGRATION_GUIDE.md | 61 +++++++++ addons/dine360_uber/__init__.py | 2 + addons/dine360_uber/__manifest__.py | 29 ++++ addons/dine360_uber/controllers/__init__.py | 1 + addons/dine360_uber/controllers/main.py | 32 +++++ addons/dine360_uber/data/uber_cron_data.xml | 15 +++ addons/dine360_uber/models/__init__.py | 3 + addons/dine360_uber/models/pos_order.py | 126 ++++++++++++++++++ addons/dine360_uber/models/pos_order_line.py | 17 +++ addons/dine360_uber/models/uber_config.py | 35 +++++ .../dine360_uber/security/ir.model.access.csv | 3 + .../dine360_uber/static/description/icon.png | Bin 0 -> 27026 bytes addons/dine360_uber/static/src/js/uber_pos.js | 40 ++++++ .../dine360_uber/static/src/xml/uber_pos.xml | 9 ++ addons/dine360_uber/views/pos_order_views.xml | 75 +++++++++++ .../dine360_uber/views/uber_config_views.xml | 65 +++++++++ upgrade_log.txt | Bin 0 -> 13762 bytes 17 files changed, 513 insertions(+) create mode 100644 UBER_INTEGRATION_GUIDE.md create mode 100644 addons/dine360_uber/__init__.py create mode 100644 addons/dine360_uber/__manifest__.py create mode 100644 addons/dine360_uber/controllers/__init__.py create mode 100644 addons/dine360_uber/controllers/main.py create mode 100644 addons/dine360_uber/data/uber_cron_data.xml create mode 100644 addons/dine360_uber/models/__init__.py create mode 100644 addons/dine360_uber/models/pos_order.py create mode 100644 addons/dine360_uber/models/pos_order_line.py create mode 100644 addons/dine360_uber/models/uber_config.py create mode 100644 addons/dine360_uber/security/ir.model.access.csv create mode 100644 addons/dine360_uber/static/description/icon.png create mode 100644 addons/dine360_uber/static/src/js/uber_pos.js create mode 100644 addons/dine360_uber/static/src/xml/uber_pos.xml create mode 100644 addons/dine360_uber/views/pos_order_views.xml create mode 100644 addons/dine360_uber/views/uber_config_views.xml create mode 100644 upgrade_log.txt 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 0000000000000000000000000000000000000000..187a8414c0f3306fb4d0e99eb1255428007f2361 GIT binary patch literal 27026 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 WdEzzx7xleLe_hov`nOOU6Y@V+7x$h3 literal 0 HcmV?d00001 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 @@ + + + + + + + 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 + + + + + + + +