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 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: if order.is_uber_order and order.uber_status and order.uber_status != 'cancelled': continue # 1. Get Configuration config = self.env['uber.config'].search([('active', '=', True)], limit=1) if not config: raise UserError(_("Uber Integration is not configured. Please check Settings.")) customer_id = config.customer_id if not customer_id: raise UserError(_("Uber Customer ID is missing in configuration.")) # 2. Get Partner (Customer) partner = order.partner_id if not partner: raise UserError(_("Customer is required for Uber delivery.")) if not partner.street or not partner.city or not partner.zip: raise UserError(_("Customer address is incomplete (Street, City, Zip required).")) # 3. Authenticate try: access_token = config._get_access_token() except Exception as e: raise UserError(_("Authentication Failed: %s") % str(e)) # 4. Prepare Payload company = order.company_id # Pickup Location (Restaurant) pickup_address = json.dumps({ "street_address": [company.street], "city": company.city, "state": company.state_id.code or "", "zip_code": company.zip, "country": company.country_id.code or "US" }) # Dropoff (Customer) dropoff_address = json.dumps({ "street_address": [partner.street], "city": partner.city, "state": partner.state_id.code or "", "zip_code": partner.zip, "country": partner.country_id.code or "US" }) items = [] for line in order.lines: if not line.product_id.is_kitchen_item: # Optional filter continue items.append({ "name": line.full_product_name or line.product_id.name, "quantity": int(line.qty), "price": int(line.price_unit * 100), # Cents "currency_code": order.currency_id.name }) if not items: # Fallback if no kitchen items found to at least send something items.append({"name": "Food Order", "quantity": 1, "price": int(order.amount_total * 100), "currency_code": order.currency_id.name}) payload = { "pickup_name": company.name, "pickup_address": pickup_address, "pickup_phone_number": company.phone or "+15555555555", "dropoff_name": partner.name, "dropoff_address": dropoff_address, "dropoff_phone_number": partner.phone or partner.mobile or "+15555555555", "manifest_items": items, "test_specifications": {"robo_courier_specification": {"mode": "auto"}} if config.environment == 'sandbox' else None } # 5. Call API api_url = f"https://api.uber.com/v1/customers/{customer_id}/deliveries" headers = { 'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json' } try: # Note: Sending the request directly to create delivery response = requests.post(api_url, headers=headers, json=payload) response.raise_for_status() data = response.json() # 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 if 'fee' in data: # Fee is in cents (minor units), convert to major units delivery_fee = float(data['fee']) / 100.0 order.write({ 'uber_status': 'pending', 'is_uber_order': True, 'uber_delivery_id': data.get('id'), 'uber_request_time': fields.Datetime.now(), 'uber_delivery_fee': delivery_fee, 'uber_tracking_url': data.get('tracking_url'), 'uber_eta': fields.Datetime.now() + datetime.timedelta(minutes=30) # Ideally parse `estimated_dropoff_time` }) # Add charge to bill if delivery_fee > 0: order._add_uber_delivery_fee(delivery_fee) except requests.exceptions.HTTPError as e: error_msg = f"Uber API Error {e.response.status_code}: {e.response.text}" raise UserError(_(error_msg)) except Exception as e: raise UserError(_("Failed to request delivery: %s") % str(e)) 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: 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, { 'product_id': config.delivery_product_id.id, 'full_product_name': config.delivery_product_id.name, 'price_unit': amount, 'qty': 1, 'tax_ids': [(6, 0, config.delivery_product_id.taxes_id.ids)], 'price_subtotal': taxes['total_excluded'], 'price_subtotal_incl': taxes['total_included'], })]}) 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', }