232 lines
10 KiB
Python

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',
}