forked from alaguraj/odoo-testing-addons
Add Kitchen Display System (KDS), Uber integration, custom theme with contact form, and POS order cleanup scripts.
This commit is contained in:
parent
889af15bd0
commit
485b6b1b61
@ -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',
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
from . import pos_order_line
|
||||
from . import product
|
||||
from . import pos_session
|
||||
from . import website_sale_integration
|
||||
|
||||
99
addons/dine360_kds/models/website_sale_integration.py
Normal file
99
addons/dine360_kds/models/website_sale_integration.py
Normal 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.")
|
||||
@ -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",
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -0,0 +1 @@
|
||||
from . import controllers
|
||||
Binary file not shown.
1
addons/dine360_theme_chennora/controllers/__init__.py
Normal file
1
addons/dine360_theme_chennora/controllers/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import main
|
||||
58
addons/dine360_theme_chennora/controllers/main.py
Normal file
58
addons/dine360_theme_chennora/controllers/main.py
Normal 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')
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
13
cleanup_bad_orders.py
Normal 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')
|
||||
@ -1,4 +1,3 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
db:
|
||||
image: postgres:15
|
||||
|
||||
70
fix_pos_references.py
Normal file
70
fix_pos_references.py
Normal 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?")
|
||||
Loading…
x
Reference in New Issue
Block a user