-
+
@@ -203,5 +204,24 @@
+
+
+
+
+
+
+
+
+
+
Thank You!
+
Your message has been successfully sent to us.
We will get back to you as soon as possible.
+
Back to Home
+
+
+
+
+
+
+
diff --git a/addons/dine360_uber/models/pos_order.py b/addons/dine360_uber/models/pos_order.py
index e40fc0e..bfd48fe 100644
--- a/addons/dine360_uber/models/pos_order.py
+++ b/addons/dine360_uber/models/pos_order.py
@@ -39,28 +39,129 @@ class PosOrder(models.Model):
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"""
+ """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
- # 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)
+ # 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"
})
- # AUTOMATICALLY ADD CHARGE TO BILL
- order._add_uber_delivery_fee(simulated_fee)
+ # 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
+ })
- # order.message_post(body="Uber Direct delivery requested. Estimated Fee: %.2f. ETA: %s" % (simulated_fee, order.uber_eta))
+ 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"""
@@ -69,11 +170,15 @@ class PosOrder(models.Model):
# 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):
diff --git a/addons/dine360_uber/models/uber_config.py b/addons/dine360_uber/models/uber_config.py
index f7fbb55..0f7cf5d 100644
--- a/addons/dine360_uber/models/uber_config.py
+++ b/addons/dine360_uber/models/uber_config.py
@@ -1,4 +1,8 @@
-from odoo import models, fields, api
+from odoo import models, fields, api, _
+from odoo.exceptions import UserError
+import requests
+import json
+import datetime
class UberConfig(models.Model):
_name = 'uber.config'
@@ -12,6 +16,7 @@ class UberConfig(models.Model):
('sandbox', 'Sandbox / Testing'),
('production', 'Production / Live')
], string='Environment', default='sandbox', required=True)
+ scope = fields.Char(string='OAuth Scope', default='delivery', help="Space-separated list of scopes, e.g., 'eats.deliveries' or 'delivery'. check your Uber Dashboard.")
timeout_minutes = fields.Integer(string='Driver Assignment Alert Timeout (min)', default=15)
delivery_product_id = fields.Many2one('product.product', string='Uber Delivery Fee Product',
@@ -22,14 +27,132 @@ class UberConfig(models.Model):
active = fields.Boolean(default=True)
+ def _get_api_base_url(self):
+ """Return the API base URL based on environment"""
+ self.ensure_one()
+ # Uber Direct API v1
+ return "https://api.uber.com/v1"
+
+ def _get_access_token(self):
+ """Get or refresh OAuth 2.0 access token"""
+ self.ensure_one()
+ now = fields.Datetime.now()
+
+ # Return existing valid token
+ if self.access_token and self.token_expiry and self.token_expiry > now:
+ return self.access_token
+
+ # Clean credentials
+ client_id = self.client_id.strip() if self.client_id else ''
+ client_secret = self.client_secret.strip() if self.client_secret else ''
+ scope = self.scope.strip() if self.scope else 'delivery'
+
+ # Request new token
+ token_url = "https://login.uber.com/oauth/v2/token"
+ payload = {
+ 'client_id': client_id,
+ 'client_secret': client_secret,
+ 'grant_type': 'client_credentials',
+ 'scope': scope # Required scope for Uber Direct
+ }
+
+ try:
+ response = requests.post(token_url, data=payload)
+ response.raise_for_status()
+ data = response.json()
+
+ access_token = data.get('access_token')
+ expires_in = data.get('expires_in', 2592000) # Default 30 days
+
+ # Save token
+ self.write({
+ 'access_token': access_token,
+ 'token_expiry': now + datetime.timedelta(seconds=expires_in - 60) # Buffer
+ })
+ return access_token
+
+ except requests.exceptions.RequestException as e:
+ error_msg = str(e)
+ if e.response is not None:
+ try:
+ error_data = e.response.json()
+ if 'error' in error_data:
+ error_msg = f"{error_data.get('error')}: {error_data.get('error_description', '')}"
+ except ValueError:
+ error_msg = e.response.text
+ raise UserError(_("Authentication Failed: %s") % error_msg)
+
def action_test_connection(self):
- # Placeholder for connection logic
+ """Test connection and auto-detect correct scope if 'invalid_scope' error occurs"""
+ self.ensure_one()
+
+ # 1. Try with current configured scope first
+ try:
+ token = self._get_access_token()
+ message = f"Connection Successful! Token retrieved using scope: {self.scope}"
+ msg_type = "success"
+ return self._return_notification(message, msg_type)
+ except UserError as e:
+ # Only attempt auto-fix if error is related to scope
+ if "invalid_scope" not in str(e) and "scope" not in str(e).lower():
+ return self._return_notification(f"Connection Failed: {str(e)}", "danger")
+
+ # 2. Auto-Discovery: Try known Uber Direct scopes
+ potential_scopes = ['delivery', 'eats.deliveries', 'direct.organizations', 'guest.deliveries']
+
+ # Remove current scope from list to avoid redundant check
+ current = self.scope.strip() if self.scope else ''
+ if current in potential_scopes:
+ potential_scopes.remove(current)
+
+ working_scope = None
+
+ for trial_scope in potential_scopes:
+ try:
+ # Temporarily set scope to test
+ self._auth_with_scope(trial_scope)
+ working_scope = trial_scope
+ break # Found one!
+ except Exception:
+ continue # Try next
+
+ # 3. Handle Result
+ if working_scope:
+ self.write({'scope': working_scope})
+ self._get_access_token() # Refresh token storage
+ message = f"Success! We found the correct scope '{working_scope}' and updated your settings."
+ msg_type = "success"
+ else:
+ message = "Connection Failed. Your Client ID does not appear to have ANY Uber Direct permissions (eats.deliveries, delivery, etc). Please enabling the 'Uber Direct' product in your Uber Dashboard."
+ msg_type = "danger"
+
+ return self._return_notification(message, msg_type)
+
+ def _auth_with_scope(self, scope_to_test):
+ """Helper to test a specific scope without saving"""
+ client_id = self.client_id.strip() if self.client_id else ''
+ client_secret = self.client_secret.strip() if self.client_secret else ''
+
+ token_url = "https://login.uber.com/oauth/v2/token"
+ payload = {
+ 'client_id': client_id,
+ 'client_secret': client_secret,
+ 'grant_type': 'client_credentials',
+ 'scope': scope_to_test
+ }
+
+ response = requests.post(token_url, data=payload)
+ response.raise_for_status() # Will raise error if scope invalid
+ return True
+
+ def _return_notification(self, message, msg_type):
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Connection Test',
- 'message': 'Uber API Connection successful (Simulation)',
- 'sticky': False,
+ 'message': message,
+ 'type': msg_type,
+ 'sticky': False if msg_type == 'success' else True,
}
}
diff --git a/addons/dine360_uber/views/uber_config_views.xml b/addons/dine360_uber/views/uber_config_views.xml
index 40372c0..1c4717b 100644
--- a/addons/dine360_uber/views/uber_config_views.xml
+++ b/addons/dine360_uber/views/uber_config_views.xml
@@ -27,6 +27,7 @@
+
diff --git a/cleanup_bad_orders.py b/cleanup_bad_orders.py
new file mode 100644
index 0000000..82e1db7
--- /dev/null
+++ b/cleanup_bad_orders.py
@@ -0,0 +1,13 @@
+from odoo import api, SUPERUSER_ID
+
+env = api.Environment(cr, SUPERUSER_ID, {})
+
+# Find bad orders with WEB/ prefix
+bad_orders = env['pos.order'].search([('pos_reference', 'like', 'WEB/%')])
+print('Found bad orders:', bad_orders.mapped('pos_reference'))
+
+if bad_orders:
+ bad_orders.unlink()
+ print('Deleted', len(bad_orders), 'bad orders')
+else:
+ print('No bad orders found')
diff --git a/docker-compose.yml b/docker-compose.yml
index d86dd6c..cea3bc2 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,4 +1,3 @@
-version: "3.8"
services:
db:
image: postgres:15
diff --git a/fix_pos_references.py b/fix_pos_references.py
new file mode 100644
index 0000000..c9d5cee
--- /dev/null
+++ b/fix_pos_references.py
@@ -0,0 +1,70 @@
+import re
+from odoo import api, SUPERUSER_ID
+
+env = api.Environment(cr, SUPERUSER_ID, {})
+
+# The pattern Odoo core uses: re.search('([0-9-]){14,}', order.pos_reference).group(0)
+# It needs at least 14 consecutive digits or hyphens, e.g. "00001-001-0001"
+PATTERN = re.compile(r'([0-9-]){14,}')
+
+print("=" * 60)
+print("Scanning ALL pos.orders for bad pos_reference values...")
+print("=" * 60)
+
+# Find all orders that are in an open session (these are the ones loaded on POS open)
+open_sessions = env['pos.session'].search([('state', '=', 'opened')])
+print(f"Open sessions found: {open_sessions.mapped('name')}")
+
+bad_orders = []
+all_orders = env['pos.order'].search([('session_id', 'in', open_sessions.ids)])
+print(f"Total orders in open sessions: {len(all_orders)}")
+
+for order in all_orders:
+ ref = order.pos_reference or ''
+ if not PATTERN.search(ref):
+ bad_orders.append(order)
+ print(f" BAD ORDER id={order.id}, pos_reference='{ref}', name='{order.name}'")
+
+print(f"\nTotal bad orders found: {len(bad_orders)}")
+
+if bad_orders:
+ print("\nOptions:")
+ print(" 1. DELETE bad orders")
+ print(" 2. FIX pos_reference to a valid format")
+ print("\nApplying FIX: setting pos_reference to valid format...")
+
+ for order in bad_orders:
+ old_ref = order.pos_reference
+ # Generate a valid reference using the order ID padded to match the format
+ new_ref = f"Order {order.id:05d}-001-0001"
+ order.write({'pos_reference': new_ref})
+ print(f" Fixed order id={order.id}: '{old_ref}' -> '{new_ref}'")
+
+ print(f"\nFixed {len(bad_orders)} orders.")
+ print("Please restart the POS session and try again.")
+else:
+ print("\nNo bad orders in open sessions. Checking ALL orders...")
+
+ # Also check orders not in any session (orphan orders)
+ all_pos_orders = env['pos.order'].search([])
+ print(f"Total pos.orders in database: {len(all_pos_orders)}")
+
+ really_bad = []
+ for order in all_pos_orders:
+ ref = order.pos_reference or ''
+ if not PATTERN.search(ref):
+ really_bad.append(order)
+ print(f" BAD ORDER id={order.id}, pos_reference='{ref}', session={order.session_id.name}, state={order.state}")
+
+ print(f"\nTotal bad orders in entire DB: {len(really_bad)}")
+ if really_bad:
+ for order in really_bad:
+ old_ref = order.pos_reference
+ new_ref = f"Order {order.id:05d}-001-0001"
+ order.write({'pos_reference': new_ref})
+ print(f" Fixed id={order.id}: '{old_ref}' -> '{new_ref}'")
+ print(f"\nFixed {len(really_bad)} orders. Try opening POS again.")
+ else:
+ print("\nAll pos_references look valid!")
+ print("The error might be from a DIFFERENT cause.")
+ print("Check: is the pricelist_id or sequence_id returning None?")