From 485b6b1b61b38c6f538b591003e89ac6e99445d4 Mon Sep 17 00:00:00 2001 From: Alaguraj0361 Date: Mon, 23 Feb 2026 11:56:16 +0530 Subject: [PATCH] Add Kitchen Display System (KDS), Uber integration, custom theme with contact form, and POS order cleanup scripts. --- addons/dine360_kds/__manifest__.py | 2 +- addons/dine360_kds/models/__init__.py | 1 + .../models/website_sale_integration.py | 99 +++++++++++++ .../dine360_kds/static/src/js/kds_backend.js | 2 +- .../views/pos_order_line_views.xml | 8 +- addons/dine360_theme_chennora/__init__.py | 1 + .../__pycache__/__init__.cpython-310.pyc | Bin 145 -> 181 bytes .../controllers/__init__.py | 1 + .../controllers/main.py | 58 ++++++++ .../views/contact_page.xml | 62 +++++--- addons/dine360_uber/models/pos_order.py | 135 ++++++++++++++++-- addons/dine360_uber/models/uber_config.py | 131 ++++++++++++++++- .../dine360_uber/views/uber_config_views.xml | 1 + cleanup_bad_orders.py | 13 ++ docker-compose.yml | 1 - fix_pos_references.py | 70 +++++++++ 16 files changed, 538 insertions(+), 47 deletions(-) create mode 100644 addons/dine360_kds/models/website_sale_integration.py create mode 100644 addons/dine360_theme_chennora/controllers/__init__.py create mode 100644 addons/dine360_theme_chennora/controllers/main.py create mode 100644 cleanup_bad_orders.py create mode 100644 fix_pos_references.py diff --git a/addons/dine360_kds/__manifest__.py b/addons/dine360_kds/__manifest__.py index 3ab8155..af7a51e 100644 --- a/addons/dine360_kds/__manifest__.py +++ b/addons/dine360_kds/__manifest__.py @@ -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', diff --git a/addons/dine360_kds/models/__init__.py b/addons/dine360_kds/models/__init__.py index 47daf9e..35435c5 100644 --- a/addons/dine360_kds/models/__init__.py +++ b/addons/dine360_kds/models/__init__.py @@ -1,3 +1,4 @@ from . import pos_order_line from . import product from . import pos_session +from . import website_sale_integration diff --git a/addons/dine360_kds/models/website_sale_integration.py b/addons/dine360_kds/models/website_sale_integration.py new file mode 100644 index 0000000..a4212d7 --- /dev/null +++ b/addons/dine360_kds/models/website_sale_integration.py @@ -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.") diff --git a/addons/dine360_kds/static/src/js/kds_backend.js b/addons/dine360_kds/static/src/js/kds_backend.js index 987f5e4..48f04e8 100644 --- a/addons/dine360_kds/static/src/js/kds_backend.js +++ b/addons/dine360_kds/static/src/js/kds_backend.js @@ -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", diff --git a/addons/dine360_kds/views/pos_order_line_views.xml b/addons/dine360_kds/views/pos_order_line_views.xml index 4107624..d79521a 100644 --- a/addons/dine360_kds/views/pos_order_line_views.xml +++ b/addons/dine360_kds/views/pos_order_line_views.xml @@ -39,14 +39,14 @@
- +
- +
-
+ """ + + 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') diff --git a/addons/dine360_theme_chennora/views/contact_page.xml b/addons/dine360_theme_chennora/views/contact_page.xml index 8a10852..abad838 100644 --- a/addons/dine360_theme_chennora/views/contact_page.xml +++ b/addons/dine360_theme_chennora/views/contact_page.xml @@ -98,60 +98,61 @@

Get In Touch For Reservations Or Inquiries!

- -
+ +
-
+ +
-
+
-
-
+
-
-
+
- +
-
+
- @@ -164,20 +165,20 @@
-
+
- +
-
+
@@ -203,5 +204,24 @@
+ + + + 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?")