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='http', auth='none', methods=['POST'], csrf=False) def uber_delivery_webhook(self, **post): """Handle status updates from Uber Direct (standard REST JSON webhook)""" 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)) # Extract delivery ID or order ID across various Uber webhook formats 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) ) raw_status = ( 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: 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', } # 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): @http.route('/shop/uber/quote', type='json', auth='public', website=True, csrf=False) def uber_quote(self, address_data, **post): """Get Uber quote for a website address with cleaned address formatting""" order = request.website.sale_get_order() if not order: return {'success': False, 'error': 'No active order'} config = request.env['uber.config'].sudo().search([('active', '=', True)], limit=1) if not config: return {'success': False, 'error': 'Uber not configured'} company = request.website.company_id # Build STRUCTURED pickup address (Object) mapping POS exactly pickup_address = { "street_address": [company.street or "Restaurant Location"], "city": company.city or "", "state": company.state_id.code if company.state_id else "", "zip_code": company.zip or "", "country": company.country_id.code if company.country_id else "CA" } # User entered address fields street = (address_data.get('street') or '').strip() street2 = (address_data.get('street2') or '').strip() full_street = f"{street}, {street2}" if street2 else street state_input = (address_data.get('state') or address_data.get('province') or '').split('(')[0].strip() state_code = "ON" if state_input: state_record = request.env['res.country.state'].sudo().search([ ('country_id.code', '=', company.country_id.code or 'CA'), '|', ('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) dropoff_address = { "street_address": [full_street] if full_street else [company.street or "Delivery Address"], "city": city_val, "state": state_code, "zip_code": zip_val, "country": country_val } # For logging, still create strings p_str = f"{pickup_address['street_address'][0]}, {pickup_address['city']} {pickup_address['state']}" d_str = f"{dropoff_address['street_address'][0]}, {dropoff_address['city']} {dropoff_address['state']}" _logger.info("WEBSITE UBER QUOTE (STRUCTURED) -\nPickup: [%s]\nDropoff: [%s]", p_str, d_str) # POS ENCODING: The POS sends these as JSON-encoded STRINGS result = config.get_uber_quote(json.dumps(pickup_address), json.dumps(dropoff_address)) if result.get('success'): order.sudo()._add_uber_delivery_fee(result['fee_amount']) return { 'success': True, 'fee': result['fee_amount'], 'eta': result.get('estimated_arrival'), } else: _logger.warning("Uber Quote Failed: %s", result.get('error')) return result