Add Kitchen Display System (KDS), Uber integration, custom theme with contact form, and POS order cleanup scripts.

This commit is contained in:
Alaguraj0361 2026-02-23 11:56:16 +05:30
parent 889af15bd0
commit 485b6b1b61
16 changed files with 538 additions and 47 deletions

View File

@ -12,7 +12,7 @@
- Floor/Table based organization
""",
'author': 'Dine360',
'depends': ['point_of_sale', 'pos_restaurant', 'dine360_restaurant'],
'depends': ['point_of_sale', 'pos_restaurant', 'dine360_restaurant', 'sale_management', 'website_sale'],
'data': [
'security/ir.model.access.csv',
'views/pos_order_line_views.xml',

View File

@ -1,3 +1,4 @@
from . import pos_order_line
from . import product
from . import pos_session
from . import website_sale_integration

View File

@ -0,0 +1,99 @@
from odoo import models, fields, api, _
import logging
_logger = logging.getLogger(__name__)
class SaleOrder(models.Model):
_inherit = 'sale.order'
def action_confirm(self):
"""
Override to create a POS Order for KDS when a Website Order is confirmed.
"""
res = super(SaleOrder, self).action_confirm()
for order in self:
# Check if it's a website order (usually has website_id)
if order.website_id:
try:
self._create_pos_order_for_kds(order)
except Exception as e:
_logger.error(f"Failed to create POS order for Website Order {order.name}: {str(e)}")
return res
def _create_pos_order_for_kds(self, sale_order):
"""Create a POS Order based on the Sale Order details"""
# Use a savepoint so that if KDS creation fails, the main Sale Order confirmation succeeds
with self.env.cr.savepoint():
PosOrder = self.env['pos.order']
PosSession = self.env['pos.session']
PosConfig = self.env['pos.config']
# 1. Find a suitable POS Config (e.g., 'Website' or first available restaurant)
config = PosConfig.search([('module_pos_restaurant', '=', True), ('active', '=', True)], limit=1)
if not config:
_logger.warning("No active POS Restaurant configuration found. Skipping KDS creation.")
return
# 2. Find or Open a Session
session = PosSession.search([
('config_id', '=', config.id),
('state', '=', 'opened')
], limit=1)
if not session:
_logger.warning(f"No open POS session found for config {config.name}. Cannot send to KDS.")
return
# 3. Create POS Order Lines
lines_data = []
for line in sale_order.order_line:
if not line.product_id:
continue
qty = line.product_uom_qty
if qty <= 0:
continue
# Skip non-kitchen items (delivery charges, shipping, etc.)
if not line.product_id.is_kitchen_item:
continue
lines_data.append((0, 0, {
'product_id': line.product_id.id,
'qty': qty,
'price_unit': line.price_unit,
'price_subtotal': line.price_subtotal,
'price_subtotal_incl': line.price_total,
'full_product_name': line.name,
'tax_ids': [(6, 0, line.tax_id.ids)],
# Key for KDS:
'preparation_status': 'waiting',
'customer_note': 'Web Order',
}))
if not lines_data:
return
# Generate proper POS reference using sequence
pos_reference = session.config_id.sequence_id.next_by_id() if session.config_id.sequence_id else f"Order {sale_order.name}"
# 4. Create POS Order (in Draft/New state to avoid double accounting)
pos_order = PosOrder.create({
'session_id': session.id,
'company_id': sale_order.company_id.id,
'partner_id': sale_order.partner_id.id,
'pricelist_id': sale_order.pricelist_id.id or session.config_id.pricelist_id.id,
'pos_reference': pos_reference,
'lines': lines_data,
'amount_total': sale_order.amount_total,
'amount_tax': sale_order.amount_tax,
'amount_paid': 0.0, # Not processing payment in POS to avoid duplication
'amount_return': 0.0,
'note': f"From Website Order {sale_order.name}",
# 'state': 'draft', # Default is draft
})
# Trigger KDS notification (handled by create method of pos.order.line in dine360_kds)
_logger.info(f"Created POS Order {pos_order.name} from Website Order {sale_order.name} for KDS.")

View File

@ -49,7 +49,7 @@ export class KdsKanbanController extends KanbanController {
if (this.notification) {
const payload = notif.payload;
this.notification.add(
`New Order: ${payload.qty}x ${payload.product_name} - ${payload.table_name}`,
`New Order: ${payload.qty}x ${payload.product_name} - ${payload.table_name || 'Web/Takeaway'}`,
{
title: "Kitchen Display",
type: "info",

View File

@ -39,14 +39,14 @@
<div class="o_kanban_record_body small text-muted mb-3">
<div class="d-flex justify-content-between align-items-center">
<span><i class="fa fa-clock-o me-1"/> <field name="create_date" widget="relative_time"/></span>
<span><i class="fa fa-clock-o me-1"/> <field name="create_date"/></span>
<span class="text-uppercase fw-bold" style="font-size: 0.7rem;"><field name="floor_id"/></span>
</div>
</div>
<div class="o_kanban_record_bottom border-top pt-3 mt-2">
<div class="oe_kanban_bottom_left">
<field name="preparation_status" widget="label_selection" options="{'classes': {'waiting': 'warning', 'preparing': 'info', 'ready': 'success', 'served': 'muted'}}"/>
<field name="preparation_status" widget="badge" decoration-warning="preparation_status == 'waiting'" decoration-info="preparation_status == 'preparing'" decoration-success="preparation_status == 'ready'" decoration-muted="preparation_status == 'served'"/>
</div>
<div class="oe_kanban_bottom_right">
<button t-if="record.preparation_status.raw_value == 'waiting'"
@ -59,7 +59,7 @@
class="btn btn-sm btn-success px-3 shadow-sm" style="background: #28a745; border: none; border-radius: 8px;">
Mark Ready
</button>
<button t-if="record.preparation_status.raw_value == 'ready'"
<button t-if="record.preparation_status.raw_value == 'ready' and record.customer_note.raw_value != 'Web Order'"
name="action_mark_served" type="object"
class="btn btn-sm btn-outline-dark px-3 shadow-sm" style="border-radius: 8px;">
Served
@ -123,7 +123,7 @@
<field name="name">Kitchen Display System</field>
<field name="res_model">pos.order.line</field>
<field name="view_mode">kanban,tree,form</field>
<field name="domain">[('product_id.is_kitchen_item', '=', True), ('product_id.name', '!=', 'Water'), ('order_id.session_id.state', '!=', 'closed'), ('product_id.pos_categ_ids.name', '!=', 'Drinks')]</field>
<field name="domain">[('product_id.is_kitchen_item', '=', True), ('product_id.name', '!=', 'Water'), ('order_id.session_id.state', '!=', 'closed'), '|', ('product_id.pos_categ_ids', '=', False), ('product_id.pos_categ_ids.name', '!=', 'Drinks')]</field>
<field name="search_view_id" ref="view_pos_order_line_kds_search"/>
<field name="context">{'search_default_today': 1}</field>
<field name="help" type="html">

View File

@ -0,0 +1 @@
from . import controllers

View File

@ -0,0 +1 @@
from . import main

View File

@ -0,0 +1,58 @@
from odoo import http
from odoo.http import request
class ContactController(http.Controller):
@http.route('/contactus/submit', type='http', auth="public", website=True, csrf=True)
def contact_submit(self, **post):
name = post.get('name')
email = post.get('email_from')
phone = post.get('phone')
subject = post.get('subject')
message = post.get('description')
# Format the email content
email_content = f"""
<div style="font-family: Arial, sans-serif; padding: 20px; border: 1px solid #ddd; border-radius: 8px;">
<h2 style="color: #2BB1A5; border-bottom: 2px solid #FECD4F; padding-bottom: 10px;">New Contact Form Submission</h2>
<table style="width: 100%; border-collapse: collapse; margin-top: 15px;">
<tr>
<td style="padding: 8px; font-weight: bold; width: 30%;">Full Name:</td>
<td style="padding: 8px;">{name}</td>
</tr>
<tr>
<td style="padding: 8px; font-weight: bold;">Email:</td>
<td style="padding: 8px;">{email}</td>
</tr>
<tr>
<td style="padding: 8px; font-weight: bold;">Phone:</td>
<td style="padding: 8px;">{phone}</td>
</tr>
<tr>
<td style="padding: 8px; font-weight: bold;">Subject:</td>
<td style="padding: 8px;">{subject}</td>
</tr>
</table>
<div style="margin-top: 20px; padding: 15px; background-color: #F8F9FA; border-left: 4px solid #2BB1A5;">
<strong style="display: block; margin-bottom: 10px;">Message:</strong>
<div style="white-space: pre-wrap;">{message}</div>
</div>
</div>
"""
mail_values = {
'subject': f"Contact Form: {subject or 'Inquiry'} from {name}",
'body_html': email_content,
'email_to': 'alaguraj0361@gmail.com',
'email_from': request.env.user.company_id.email or 'noreply@chennora.com',
'reply_to': email,
}
# Create and send the mail
try:
mail = request.env['mail.mail'].sudo().create(mail_values)
mail.send()
except Exception as e:
# You might want to log the error
pass
return request.render('dine360_theme_chennora.contact_thank_you')

View File

@ -98,60 +98,61 @@
<div class="bg-white p-5 rounded-3 shadow-lg">
<h3 class="fw-bold mb-4" style="color: #04121D;">Get In Touch For Reservations Or Inquiries!</h3>
<!-- Odoo Form Implementation -->
<section class="s_website_form" data-vcss="001" data-snippet="s_website_form_default">
<!-- Standard HTML Form (Bypassing Odoo JS Interference) -->
<section class="s_contact_form_custom pt-3">
<div class="container">
<form action="/website/form/" method="post" enctype="multipart/form-data" class="o_mark_required" data-mark="*" data-model_name="mail.mail" data-success-mode="redirect" data-success-page="/contactus-thank-you" data-pre-fill="true">
<form action="/contactus/submit" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="s_website_form_rows row s_col_no_bgcolor">
<!-- Full Name -->
<div class="col-lg-6 mb-4 s_website_form_field" data-type="char" data-name="Field">
<div class="col-lg-6 mb-4">
<div class="s_col_no_resize s_col_no_bgcolor">
<label class="form-label fw-normal" for="contact_name" style="color: #666; font-size: 15px;">
<label class="form-label fw-normal" for="name" style="color: #666; font-size: 15px;">
Full Name
<span class="s_website_form_mark text-danger"> *</span>
<span class="text-danger"> *</span>
</label>
<div>
<input type="text" class="form-control s_website_form_input" name="name" required="1" placeholder="Full Name" style="background-color: #F8F9FA; border: 1px solid #2BB1A5 !important; border-radius: 8px; padding: 12px; color: #2BB1A5 !important; font-size: 14px;"/>
<input type="text" class="form-control" name="name" required="1" placeholder="Full Name" style="background-color: #F8F9FA; border: 1px solid #2BB1A5 !important; border-radius: 8px; padding: 12px; color: #2BB1A5 !important; font-size: 14px;"/>
</div>
</div>
</div>
<!-- Email Address -->
<div class="col-lg-6 mb-4 s_website_form_field" data-type="email" data-name="Field">
<div class="col-lg-6 mb-4">
<div class="s_col_no_resize s_col_no_bgcolor">
<label class="form-label fw-normal" for="contact_email" style="color: #666; font-size: 15px;">
<label class="form-label fw-normal" for="email_from" style="color: #666; font-size: 15px;">
Email Address
<span class="s_website_form_mark text-danger"> *</span>
<span class="text-danger"> *</span>
</label>
<div>
<input type="email" class="form-control s_website_form_input" name="email_from" required="1" placeholder="" style="background-color: #F8F9FA; border: 1px solid #2BB1A5 !important; border-radius: 8px; padding: 12px; color: #2BB1A5 !important; font-size: 14px;"/>
<input type="email" class="form-control" name="email_from" required="1" placeholder="Email Address" style="background-color: #F8F9FA; border: 1px solid #2BB1A5 !important; border-radius: 8px; padding: 12px; color: #2BB1A5 !important; font-size: 14px;"/>
</div>
</div>
</div>
<!-- Phone Number -->
<div class="col-lg-6 mb-4 s_website_form_field" data-type="tel" data-name="Field">
<div class="col-lg-6 mb-4">
<div class="s_col_no_resize s_col_no_bgcolor">
<label class="form-label fw-normal" for="phone" style="color: #666; font-size: 15px;">
Phone Number
<span class="s_website_form_mark text-danger"> *</span>
<span class="text-danger"> *</span>
</label>
<div>
<input type="tel" class="form-control s_website_form_input" name="phone" placeholder="" style="background-color: #F8F9FA; border: 1px solid #2BB1A5 !important; border-radius: 8px; padding: 12px; color: #2BB1A5 !important; font-size: 14px;"/>
<input type="tel" class="form-control" name="phone" required="1" placeholder="Phone Number" style="background-color: #F8F9FA; border: 1px solid #2BB1A5 !important; border-radius: 8px; padding: 12px; color: #2BB1A5 !important; font-size: 14px;"/>
</div>
</div>
</div>
<!-- Subject -->
<div class="col-lg-6 mb-4 s_website_form_field" data-type="char" data-name="Field">
<div class="col-lg-6 mb-4">
<div class="s_col_no_resize s_col_no_bgcolor">
<label class="form-label fw-normal" for="subject" style="color: #666; font-size: 15px;">
Subject
<span class="s_website_form_mark text-danger"> *</span>
<span class="text-danger"> *</span>
</label>
<div>
<select class="form-select s_website_form_input" name="subject" style="background-color: #F8F9FA; border: 1px solid #2BB1A5 !important; border-radius: 8px; padding: 12px; color: #2BB1A5 !important; font-size: 14px;">
<select class="form-select" name="subject" required="1" style="background-color: #F8F9FA; border: 1px solid #2BB1A5 !important; border-radius: 8px; padding: 12px; color: #2BB1A5 !important; font-size: 14px;">
<option value="" disabled="disabled" selected="selected">Select Subject</option>
<option value="Complain">Complain</option>
<option value="Greetings">Greetings</option>
@ -164,20 +165,20 @@
</div>
<!-- Message -->
<div class="col-12 mb-4 s_website_form_field" data-type="text" data-name="Field">
<div class="col-12 mb-4">
<div class="s_col_no_resize s_col_no_bgcolor">
<label class="form-label fw-normal" for="description" style="color: #666; font-size: 15px;">
Message
<span class="s_website_form_mark text-danger"> *</span>
<span class="text-danger"> *</span>
</label>
<div>
<textarea class="form-control s_website_form_input" name="description" required="1" placeholder="Message" rows="5" style="background-color: #FFFFFF; border: 1px solid #2BB1A5 !important; border-radius: 8px; padding: 12px; color: #2BB1A5 !important; font-size: 14px;"></textarea>
<textarea class="form-control" name="description" required="1" placeholder="Message" rows="5" style="background-color: #FFFFFF; border: 1px solid #2BB1A5 !important; border-radius: 8px; padding: 12px; color: #2BB1A5 !important; font-size: 14px;"></textarea>
</div>
</div>
</div>
<!-- Submit Button -->
<div class="col-12 s_website_form_submit" data-name="Submit Button">
<div class="col-12">
<button type="submit" class="btn btn-primary btn-lg w-100 fw-bold" style="background-color: #2BB1A5 !important; border: none; color: #04121D; padding: 15px; text-transform: uppercase; border-radius: 4px;">
Submit Now <i class="fa fa-long-arrow-right ms-2"></i>
</button>
@ -203,5 +204,24 @@
</div>
</xpath>
</template>
<template id="contact_thank_you" name="Contact Thank You">
<t t-call="website.layout">
<div id="wrap">
<section class="pt120 pb120" style="background-color: #F9F6F0;">
<div class="container text-center">
<div class="mb-4">
<i class="fa fa-check-circle-o fa-5x" style="color: #2BB1A5;"></i>
</div>
<h1 class="display-3 fw-bold mb-3" style="color: #04121D;">Thank You!</h1>
<p class="lead text-muted mb-5">Your message has been successfully sent to us.<br/>We will get back to you as soon as possible.</p>
<a href="/" class="btn btn-lg flat" style="background-color: #FECD4F; color: #04121D; font-weight: 700; padding: 15px 40px; border-radius: 4px; text-transform: uppercase;">Back to Home</a>
</div>
</section>
</div>
</t>
</template>
</data>
</odoo>

View File

@ -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):

View File

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

View File

@ -27,6 +27,7 @@
<field name="client_id"/>
<field name="client_secret" password="True"/>
<field name="customer_id"/>
<field name="scope" placeholder="e.g. eats.deliveries"/>
</group>
<group string="Settings">
<field name="environment"/>

13
cleanup_bad_orders.py Normal file
View File

@ -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')

View File

@ -1,4 +1,3 @@
version: "3.8"
services:
db:
image: postgres:15

70
fix_pos_references.py Normal file
View File

@ -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?")