first commit
50
.agents/workflows/testing.md
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
description: Dine360 End-to-End (E2E) Integration Testing Workflow
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🚀 Dine360 E2E Testing Workflow
|
||||||
|
|
||||||
|
This workflow ensures all modules (`Self-Order`, `Online Orders`, `KDS`, and `POS`) are communicating correctly.
|
||||||
|
|
||||||
|
## 1. Environment Check
|
||||||
|
Before testing, verify the services are up:
|
||||||
|
// turbo
|
||||||
|
`docker ps`
|
||||||
|
Ensure `odoo_client2` and `db` are in a 'Healthy' or 'Up' state.
|
||||||
|
|
||||||
|
## 2. Setup POS Session
|
||||||
|
1. Open your Odoo instance (usually `http://localhost:8069`).
|
||||||
|
2. Go to **Point of Sale**.
|
||||||
|
3. **Open** a new session for your main Shop/Restaurant.
|
||||||
|
|
||||||
|
## 3. Test Flow: Self-Order (Table QR)
|
||||||
|
1. Go to **Point of Sale > Configuration > Floor Plans**.
|
||||||
|
2. Select a floor and a table (e.g., "Table 1").
|
||||||
|
3. Click the **Open Front-end** button (this opens the Self-Order menu).
|
||||||
|
4. **Action**: Add 2-3 items to the cart and click **Send to Kitchen**.
|
||||||
|
5. **Verification**:
|
||||||
|
- [ ] Go to the **Kitchen (KDS)** module.
|
||||||
|
- [ ] Check that the items appear in the **Waiting** column.
|
||||||
|
- [ ] Confirm the source badge shows **QR Table Order / Table 1**.
|
||||||
|
|
||||||
|
## 4. Test Flow: Online Orders (Website)
|
||||||
|
1. Navigate to the Website Shop (`/shop`).
|
||||||
|
2. **Action**: Add items to the cart, proceed to checkout, and complete the order.
|
||||||
|
3. **Internal POS Verification**:
|
||||||
|
- [ ] Open the POS UI.
|
||||||
|
- [ ] Click the **Online Orders** tab in the top navbar.
|
||||||
|
- [ ] Select your order and click **Confirm & Send to Kitchen**.
|
||||||
|
4. **KDS Verification**:
|
||||||
|
- [ ] Check the **Kitchen (KDS)** module.
|
||||||
|
- [ ] Source badge should show **Online / eCommerce**.
|
||||||
|
|
||||||
|
## 5. Test Flow: KDS Management
|
||||||
|
1. In the **Kitchen (KDS)** dashboard:
|
||||||
|
2. **Action**: Click the **Preparing** button on one of the cards.
|
||||||
|
3. **Action**: Click the **Ready** button when finished.
|
||||||
|
4. **Verification**:
|
||||||
|
- [ ] Confirm the item moves to the correct column.
|
||||||
|
- [ ] If you are in the POS UI, check if any notifications appear regarding readiness (if implemented).
|
||||||
|
|
||||||
|
## 6. Verification Summary
|
||||||
|
If all checks above pass, the integration between the Frontend (Customer), Middle-end (POS), and Backend (KDS) is working perfectly.
|
||||||
96
.gitignore
vendored
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
# Python artifacts
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
.pytest_cache/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Odoo artifacts
|
||||||
|
*.log
|
||||||
|
odoo.conf
|
||||||
|
/data/
|
||||||
|
/filestore/
|
||||||
|
/sessions/
|
||||||
|
/logs/
|
||||||
|
/dump/
|
||||||
|
|
||||||
|
# OS artifacts
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# IDEs
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
.history/
|
||||||
|
*.sublime-project
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Debug/Temp scripts found in project
|
||||||
|
inspect_*.py
|
||||||
|
dump_*.py
|
||||||
|
debug_*.py
|
||||||
|
read_arch.py
|
||||||
|
resolve_homepage.py
|
||||||
|
fix_homepage.py
|
||||||
|
force_inherit.py
|
||||||
|
txt.py
|
||||||
|
update_error.txt
|
||||||
|
update_log.txt
|
||||||
|
addons/*.png
|
||||||
|
blog_posts_*.json
|
||||||
|
|
||||||
|
# Local config/environment
|
||||||
|
.env
|
||||||
|
docker-compose.override.yml
|
||||||
|
|
||||||
|
# Log / debug dump files
|
||||||
|
*.txt
|
||||||
|
*.log
|
||||||
|
*_logs.txt
|
||||||
|
shop_output.html
|
||||||
|
upgrade_log.txt
|
||||||
|
|
||||||
|
# Database dumps
|
||||||
|
*.sql
|
||||||
|
*.sqlite
|
||||||
|
*.dump
|
||||||
|
|
||||||
|
# One-off scripts (keep .ps1 compose scripts but ignore others)
|
||||||
|
cleanup_*.py
|
||||||
|
fix_*.py
|
||||||
|
test_*.py
|
||||||
|
temp_*.py
|
||||||
113
README.md
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
# Dine360 Odoo Addons_New by mohan1
|
||||||
|
|
||||||
|
This repository contains custom Odoo 17 addons for the Dine360 Restaurant Suite. It includes a website theme, a custom login/dashboard experience, and a restaurant role-based access module, all bundled by a meta module for one-click install.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
- Odoo 17 (Docker)
|
||||||
|
- Postgres 15 (Docker)
|
||||||
|
- Addons mounted from `./addons`
|
||||||
|
|
||||||
|
## Services (Docker)
|
||||||
|
- Odoo: `http://localhost:10001`
|
||||||
|
- DB: Postgres 15 (`odoo` / `odoo`)
|
||||||
|
|
||||||
|
## Repository Layout
|
||||||
|
- `addons/` – Odoo addons
|
||||||
|
- `docker-compose.yml` – Odoo + Postgres stack
|
||||||
|
- `backup_db.ps1`, `export_odoo.ps1` – Windows helpers
|
||||||
|
- `inspect_views.py`, `resolve_homepage.py`, etc. – view debugging helpers
|
||||||
|
|
||||||
|
## Addons
|
||||||
|
|
||||||
|
### 1) `dine360_dashboard`
|
||||||
|
Custom login layout and app-grid dashboard landing page.
|
||||||
|
|
||||||
|
Key features:
|
||||||
|
- Redirect `/web/login` to `/` after successful login
|
||||||
|
- Override `/` for authenticated users to show a custom app dashboard
|
||||||
|
- Custom login page layout and styling
|
||||||
|
|
||||||
|
Key files:
|
||||||
|
- `addons/dine360_dashboard/controllers/main.py`
|
||||||
|
- `addons/dine360_dashboard/views/home_template.xml`
|
||||||
|
- `addons/dine360_dashboard/views/login_templates.xml`
|
||||||
|
- `addons/dine360_dashboard/views/web_title_template.xml`
|
||||||
|
- `addons/dine360_dashboard/views/website_logo.xml`
|
||||||
|
- `addons/dine360_dashboard/static/src/css/*`
|
||||||
|
|
||||||
|
### 2) `dine360_restaurant`
|
||||||
|
Role-based access control for restaurant staff.
|
||||||
|
|
||||||
|
Roles:
|
||||||
|
- Admin/Owner, Manager, Cashier, Waiter/Captain, Kitchen (KDS), Store Keeper
|
||||||
|
|
||||||
|
Key files:
|
||||||
|
- `addons/dine360_restaurant/models/res_users.py`
|
||||||
|
- `addons/dine360_restaurant/security/*`
|
||||||
|
- `addons/dine360_restaurant/views/*`
|
||||||
|
|
||||||
|
### 3) `dine360_theme_shivasakthi`
|
||||||
|
Custom website theme and page content (homepage + contact us).
|
||||||
|
|
||||||
|
Key files:
|
||||||
|
- `addons/dine360_theme_shivasakthi/views/layout.xml`
|
||||||
|
- `addons/dine360_theme_shivasakthi/views/pages.xml`
|
||||||
|
- `addons/dine360_theme_shivasakthi/static/src/scss/*`
|
||||||
|
- `addons/dine360_theme_shivasakthi/static/src/img/*`
|
||||||
|
|
||||||
|
### 4) `Dine360_Shivasakthi` (meta module)
|
||||||
|
Install this single module to pull in all required addons.
|
||||||
|
|
||||||
|
Depends on:
|
||||||
|
- `dine360_dashboard`
|
||||||
|
- `dine360_restaurant`
|
||||||
|
- `dine360_theme_shivasakthi`
|
||||||
|
|
||||||
|
## Standard Install / Upgrade
|
||||||
|
|
||||||
|
### Start the stack
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Apps list
|
||||||
|
- Apps -> Update Apps List
|
||||||
|
|
||||||
|
### Install the suite (recommended)
|
||||||
|
- Apps -> search `Dine360 Restaurant Suite` -> Install
|
||||||
|
|
||||||
|
### Upgrade the suite (after code changes)
|
||||||
|
```bash
|
||||||
|
docker exec odoo_client1 odoo -u Dine360_Shivasakthi -d shivasakthi_db --db_host db --db_user odoo --db_password odoo --stop-after-init
|
||||||
|
```
|
||||||
|
|
||||||
|
### Upgrade a single addon
|
||||||
|
```bash
|
||||||
|
docker exec odoo_client1 odoo -u dine360_dashboard -d shivasakthi_db --db_host db --db_user odoo --db_password odoo --stop-after-init
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logos (Apps icons)
|
||||||
|
Place PNGs here (128x128 or 256x256 recommended):
|
||||||
|
- `addons/Dine360_Shivasakthi/static/description/icon.png`
|
||||||
|
- `addons/dine360_dashboard/static/description/icon.png`
|
||||||
|
- `addons/dine360_restaurant/static/description/icon.png`
|
||||||
|
- `addons/dine360_theme_shivasakthi/static/description/icon.png`
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### 500 error after view edits
|
||||||
|
- Upgrade the affected module
|
||||||
|
- Restart Odoo: `docker-compose restart odoo`
|
||||||
|
- Hard refresh browser (Ctrl + F5)
|
||||||
|
|
||||||
|
### Old modules still present (home_dashboard, restaurant_management, theme_shivasakthi)
|
||||||
|
If you renamed modules, uninstall the old ones in Apps to avoid conflicts.
|
||||||
|
|
||||||
|
## Utilities
|
||||||
|
Helper scripts for view diagnostics and homepage issues:
|
||||||
|
- `inspect_views.py`, `inspect_views_v2.py`
|
||||||
|
- `resolve_homepage.py`, `fix_homepage.py`, `force_inherit.py`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Homepage content is fully overridden in `addons/dine360_theme_shivasakthi/views/pages.xml`.
|
||||||
|
- If theme changes don’t appear, check for COW (customized) views masking the theme.
|
||||||
2
addons/Dine360_Shivasakthi/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Meta module for Dine360 Shivasakthi
|
||||||
|
from .hooks import uninstall_hook
|
||||||
44
addons/Dine360_Shivasakthi/__manifest__.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
'name': 'Dine360 Shivasakthi Restaurant Suite',
|
||||||
|
'version': '1.0.0',
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
'category': 'Website',
|
||||||
|
'summary': 'Installs all Dine360 Shivasakthi Restaurant modules',
|
||||||
|
'author': 'Dine360',
|
||||||
|
'depends': [
|
||||||
|
'dine360_restaurant',
|
||||||
|
'dine360_order_channels',
|
||||||
|
'dine360_dashboard',
|
||||||
|
'dine360_theme_shivasakthi',
|
||||||
|
'dine360_kds',
|
||||||
|
'dine360_reservation',
|
||||||
|
'dine360_uber',
|
||||||
|
'dine360_recipe',
|
||||||
|
'dine360_self_order',
|
||||||
|
'dine360_online_orders',
|
||||||
|
'dine360_pos_navbar',
|
||||||
|
'mail',
|
||||||
|
'calendar',
|
||||||
|
'contacts',
|
||||||
|
'crm',
|
||||||
|
'sale_management',
|
||||||
|
'board',
|
||||||
|
'point_of_sale',
|
||||||
|
'account',
|
||||||
|
'website',
|
||||||
|
'purchase',
|
||||||
|
'stock',
|
||||||
|
'hr',
|
||||||
|
],
|
||||||
|
'uninstall_hook': 'uninstall_hook',
|
||||||
|
'data': [
|
||||||
|
'views/apps_kanban_menu.xml',
|
||||||
|
],
|
||||||
|
'assets': {
|
||||||
|
'web.assets_backend': [
|
||||||
|
'Dine360_Shivasakthi/static/src/css/apps_kanban_fix.css',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'installable': True,
|
||||||
|
'application': True,
|
||||||
|
}
|
||||||
24
addons/Dine360_Shivasakthi/hooks.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from odoo import api, SUPERUSER_ID
|
||||||
|
|
||||||
|
def uninstall_hook(env):
|
||||||
|
"""
|
||||||
|
Synchronized uninstallation: When Dine360 Shivasakthi Restaurant Suite is uninstalled,
|
||||||
|
automatically trigger uninstallation for all its core sub-modules.
|
||||||
|
"""
|
||||||
|
modules_to_uninstall = [
|
||||||
|
'dine360_dashboard',
|
||||||
|
'dine360_restaurant',
|
||||||
|
'dine360_theme_shivasakthi',
|
||||||
|
'dine360_kds',
|
||||||
|
'dine360_reservation'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Search for these modules if they are installed
|
||||||
|
modules = env['ir.module.module'].search([
|
||||||
|
('name', 'in', modules_to_uninstall),
|
||||||
|
('state', '=', 'installed')
|
||||||
|
])
|
||||||
|
|
||||||
|
if modules:
|
||||||
|
# Mark modules for uninstallation
|
||||||
|
modules.button_uninstall()
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
/* Fix Apps kanban icon sizing and dropdown overlap */
|
||||||
|
.o_modules_kanban .oe_module_vignette,
|
||||||
|
.o_modules_kanban .o_kanban_record {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_modules_kanban .o_kanban_record {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_modules_kanban .oe_module_icon {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_modules_kanban .oe_module_icon img {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_modules_kanban .oe_module_vignette .dropdown-menu {
|
||||||
|
right: 0;
|
||||||
|
left: auto;
|
||||||
|
min-width: 160px;
|
||||||
|
z-index: 1060;
|
||||||
|
}
|
||||||
15
addons/Dine360_Shivasakthi/views/apps_kanban_menu.xml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<template id="dine360_apps_kanban_menu_shivasakthi" inherit_id="base.module_view_kanban" name="Dine360 Apps Kanban Menu Shivasakthi">
|
||||||
|
<xpath expr="//t[@t-name='kanban-menu']" position="replace">
|
||||||
|
<t t-name="kanban-menu">
|
||||||
|
<t t-set="installed" t-value="record.state.raw_value == 'installed'"/>
|
||||||
|
<a type="edit" class="dropdown-item">Module Info</a>
|
||||||
|
<a t-if="record.website.raw_value" role="menuitem" class="dropdown-item o-hidden-ios" t-att-href="record.website.raw_value" target="_blank">Learn More</a>
|
||||||
|
<a t-if="installed" name="button_immediate_upgrade" type="object" role="menuitem" class="dropdown-item" groups="base.group_system">Upgrade</a>
|
||||||
|
<a t-if="installed" name="button_uninstall_wizard" type="object" role="menuitem" class="dropdown-item" groups="base.group_system">Uninstall</a>
|
||||||
|
</t>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
4
addons/dine360_dashboard/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# dine360_dashboard/__init__.py
|
||||||
|
from . import controllers
|
||||||
|
from . import models
|
||||||
|
|
||||||
38
addons/dine360_dashboard/__manifest__.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
'name': 'Dine360 Dashboard',
|
||||||
|
'version': '1.0.2',
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
'category': 'Website',
|
||||||
|
'summary': 'Redirect login to home and show icon grid',
|
||||||
|
'depends': ['base', 'web', 'auth_signup', 'website', 'website_sale'],
|
||||||
|
'data': [
|
||||||
|
'views/home_template.xml',
|
||||||
|
'views/login_templates.xml',
|
||||||
|
'views/web_title_template.xml',
|
||||||
|
'views/website_logo.xml',
|
||||||
|
'views/shop_template.xml',
|
||||||
|
'data/branding_data.xml',
|
||||||
|
],
|
||||||
|
'assets': {
|
||||||
|
'web.assets_backend': [
|
||||||
|
'dine360_dashboard/static/src/css/theme_variables.css',
|
||||||
|
'dine360_dashboard/static/src/css/home_menu.css',
|
||||||
|
'dine360_dashboard/static/src/js/chennora_title.js',
|
||||||
|
'dine360_dashboard/static/src/xml/navbar_extension.xml',
|
||||||
|
],
|
||||||
|
'web.assets_frontend': [
|
||||||
|
'dine360_dashboard/static/src/css/theme_variables.css',
|
||||||
|
'dine360_dashboard/static/src/css/login_style.css',
|
||||||
|
'dine360_dashboard/static/src/css/website_style.css',
|
||||||
|
'dine360_dashboard/static/src/css/shop_style.css',
|
||||||
|
],
|
||||||
|
'web.assets_common': [
|
||||||
|
'dine360_dashboard/static/src/css/theme_variables.css',
|
||||||
|
],
|
||||||
|
'point_of_sale._assets_pos': [
|
||||||
|
'dine360_dashboard/static/src/css/pos_style.css',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'installable': True,
|
||||||
|
'application': True,
|
||||||
|
}
|
||||||
4
addons/dine360_dashboard/controllers/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# dine360_dashboard/controllers/__init__.py
|
||||||
|
from . import main
|
||||||
|
from . import cors
|
||||||
|
|
||||||
15
addons/dine360_dashboard/controllers/cors.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from odoo import http
|
||||||
|
from odoo.http import request
|
||||||
|
import json
|
||||||
|
|
||||||
|
class CorsHandler(http.Controller):
|
||||||
|
# This specifically targets the authentication endpoint for CORS
|
||||||
|
@http.route('/web/session/authenticate', type='json', auth="none", cors="*")
|
||||||
|
def authenticate_cors(self, db, login, password, base_location=None):
|
||||||
|
request.session.authenticate(db, login, password)
|
||||||
|
return request.env['ir.http'].session_info()
|
||||||
|
|
||||||
|
# Generic search_read for the dashboard apps
|
||||||
|
@http.route('/web/dataset/call_kw', type='json', auth="user", cors="*")
|
||||||
|
def call_kw_cors(self, model, method, args, kwargs, path=None):
|
||||||
|
return request.env[model].with_user(request.uid).call_kw(method, args, kwargs)
|
||||||
140
addons/dine360_dashboard/controllers/main.py
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
from odoo import http
|
||||||
|
from odoo.http import request
|
||||||
|
from odoo.addons.web.controllers.home import Home
|
||||||
|
|
||||||
|
class CustomHome(Home):
|
||||||
|
@http.route('/web/login', type='http', auth="public", website=True)
|
||||||
|
def web_login(self, *args, **kw):
|
||||||
|
response = super(CustomHome, self).web_login(*args, **kw)
|
||||||
|
if request.params.get('login_success') and request.session.uid:
|
||||||
|
# Use relative redirect to maintain HTTPS/HTTP protocol
|
||||||
|
return request.redirect('/')
|
||||||
|
return response
|
||||||
|
|
||||||
|
from odoo.addons.website.controllers.main import Website
|
||||||
|
|
||||||
|
class ImageHome(Website):
|
||||||
|
@http.route('/', type='http', auth='public', website=True, sitemap=True)
|
||||||
|
def index(self, **kwargs):
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
# SUPER SAFE EDITOR & IFRAME DETECTION
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
path = request.httprequest.path
|
||||||
|
params = request.params
|
||||||
|
headers = request.httprequest.headers
|
||||||
|
referer = headers.get('Referer', '')
|
||||||
|
fetch_dest = headers.get('Sec-Fetch-Dest', '')
|
||||||
|
|
||||||
|
# 1. If not logged in, always show standard homepage
|
||||||
|
if not request.session.uid:
|
||||||
|
return super(ImageHome, self).index(**kwargs)
|
||||||
|
|
||||||
|
# 2. ROLE-BASED AUTO REDIRECTION (FOR STAFF)
|
||||||
|
# Skip the dashboard/website entirely for Chefs and Waiters
|
||||||
|
user = request.env.user.sudo()
|
||||||
|
is_admin = user.has_group('base.group_system') or \
|
||||||
|
user.has_group('dine360_restaurant.group_restaurant_admin')
|
||||||
|
|
||||||
|
if not is_admin:
|
||||||
|
# 1. WAITER / CASHIER -> Priority goes to POS
|
||||||
|
if user.has_group('dine360_restaurant.group_restaurant_waiter') or \
|
||||||
|
user.has_group('dine360_restaurant.group_restaurant_cashier'):
|
||||||
|
return request.redirect('/web#action=point_of_sale.action_client_pos_menu')
|
||||||
|
|
||||||
|
# 2. CHEF -> Directly to KDS
|
||||||
|
if user.has_group('dine360_restaurant.group_restaurant_kitchen'):
|
||||||
|
return request.redirect('/web#action=dine360_kds.action_kds_dashboard')
|
||||||
|
|
||||||
|
# 3. SUPER SAFE EDITOR & IFRAME DETECTION
|
||||||
|
path = request.httprequest.path
|
||||||
|
params = request.params
|
||||||
|
headers = request.httprequest.headers
|
||||||
|
referer = headers.get('Referer', '')
|
||||||
|
fetch_dest = headers.get('Sec-Fetch-Dest', '')
|
||||||
|
|
||||||
|
# Check for ANY editor or backend signal
|
||||||
|
editor_params = ['enable_editor', 'edit', 'path', 'website_id', 'frontend_edit', 'model', 'id']
|
||||||
|
is_editor_request = any(p in params for p in editor_params)
|
||||||
|
is_from_backend = any(m in referer for m in ['/website/force', 'enable_editor'])
|
||||||
|
|
||||||
|
# if it looks like Odoo internal business, return the real website
|
||||||
|
if fetch_dest == 'iframe' or is_editor_request or is_from_backend:
|
||||||
|
return super(ImageHome, self).index(**kwargs)
|
||||||
|
|
||||||
|
if path != '/':
|
||||||
|
return super(ImageHome, self).index(**kwargs)
|
||||||
|
|
||||||
|
# Remove sudo() to respect Odoo's standard menu group restrictions
|
||||||
|
menus = request.env['ir.ui.menu'].search([
|
||||||
|
('parent_id', '=', False)
|
||||||
|
], order='sequence')
|
||||||
|
|
||||||
|
# User role checks
|
||||||
|
try:
|
||||||
|
is_admin = request.env.user.has_group('base.group_system') or \
|
||||||
|
request.env.user.has_group('dine360_restaurant.group_restaurant_admin')
|
||||||
|
is_kitchen = request.env.user.has_group('dine360_restaurant.group_restaurant_kitchen')
|
||||||
|
except Exception:
|
||||||
|
is_admin = request.env.user.has_group('base.group_system')
|
||||||
|
is_kitchen = False
|
||||||
|
|
||||||
|
# User requested to hide all standard apps and POS/KDS.
|
||||||
|
# Only allow specific menus based on user request + admin tools.
|
||||||
|
allowed_menus = ['Online Orders', 'Website', 'Table Reservations', 'Uber Integration', 'Apps', 'Settings']
|
||||||
|
|
||||||
|
filtered_menus = []
|
||||||
|
seen_names = set()
|
||||||
|
for menu in menus:
|
||||||
|
# Match strictly against allowed menus
|
||||||
|
if menu.name not in allowed_menus and menu.name != 'Table Reservation':
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Hide "Apps" and "Settings" for non-admins
|
||||||
|
if menu.name in ['Apps', 'Settings'] and not is_admin:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# De-duplicate by name
|
||||||
|
if menu.name in seen_names:
|
||||||
|
continue
|
||||||
|
seen_names.add(menu.name)
|
||||||
|
|
||||||
|
# Dynamic Icon Override (Dine360 Branding)
|
||||||
|
icon_mapping = {
|
||||||
|
'Apps': 'dine360_dashboard,static/src/img/icons/apps.svg',
|
||||||
|
'Settings': 'dine360_dashboard,static/src/img/icons/settings.svg',
|
||||||
|
'Table Reservation': 'dine360_dashboard,static/src/img/icons/table_reservation.svg',
|
||||||
|
'Table Reservations': 'dine360_dashboard,static/src/img/icons/table_reservation.svg',
|
||||||
|
'Uber Integration': 'dine360_dashboard,static/src/img/icons/uber_integration.svg',
|
||||||
|
'Online Orders': 'dine360_dashboard,static/src/img/icons/website.svg',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Find the best match in the mapping
|
||||||
|
current_name = menu.name
|
||||||
|
for key, icon_path in icon_mapping.items():
|
||||||
|
if key.lower() in current_name.lower():
|
||||||
|
menu.web_icon = icon_path
|
||||||
|
break
|
||||||
|
|
||||||
|
filtered_menus.append(menu)
|
||||||
|
|
||||||
|
# Low Stock Alerts (Ingredients)
|
||||||
|
low_stock_products = []
|
||||||
|
try:
|
||||||
|
ProductTemplate = request.env['product.template'].sudo()
|
||||||
|
if hasattr(ProductTemplate, 'get_low_stock_products'):
|
||||||
|
low_stock_products = ProductTemplate.get_low_stock_products(limit=5)
|
||||||
|
except Exception:
|
||||||
|
low_stock_products = []
|
||||||
|
|
||||||
|
return request.render('dine360_dashboard.image_home_template', {
|
||||||
|
'menus': filtered_menus,
|
||||||
|
'user_id': request.env.user,
|
||||||
|
'low_stock_products': low_stock_products
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@http.route('/home', type='http', auth="public", website=True, sitemap=True)
|
||||||
|
def website_home(self, **kw):
|
||||||
|
# Explicit route for standard Website Homepage
|
||||||
|
return request.render('website.homepage')
|
||||||
17
addons/dine360_dashboard/data/branding_data.xml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="base_title_param" model="ir.config_parameter">
|
||||||
|
<field name="key">web.base_title</field>
|
||||||
|
<field name="value">Shivasakthi</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
<data>
|
||||||
|
<record id="base.main_company" model="res.company">
|
||||||
|
<field name="phone">+1(647)856-2878</field>
|
||||||
|
</record>
|
||||||
|
<!-- Note: 'phone' field is not available on the website model in Odoo 17.
|
||||||
|
Phone/contact info should be set on res.company (line above) instead. -->
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
||||||
1
addons/dine360_dashboard/models/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import ir_ui_menu
|
||||||
34
addons/dine360_dashboard/models/ir_ui_menu.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from odoo import models, api
|
||||||
|
|
||||||
|
class IrUiMenu(models.Model):
|
||||||
|
_inherit = 'ir.ui.menu'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def load_menus(self, debug):
|
||||||
|
"""
|
||||||
|
Override standard menu loading to hide unwanted root menus.
|
||||||
|
"""
|
||||||
|
menus = super(IrUiMenu, self).load_menus(debug)
|
||||||
|
|
||||||
|
user = self.env.user
|
||||||
|
is_admin = user.has_group('base.group_system')
|
||||||
|
|
||||||
|
allowed_menus = ['Online Orders', 'Website', 'Table Reservations', 'Uber Integration', 'Apps', 'Settings']
|
||||||
|
|
||||||
|
if 'root' in menus and 'children' in menus['root']:
|
||||||
|
new_children = []
|
||||||
|
for child_id in menus['root']['children']:
|
||||||
|
child_menu = menus.get(child_id)
|
||||||
|
if not child_menu:
|
||||||
|
continue
|
||||||
|
name = child_menu.get('name')
|
||||||
|
|
||||||
|
# Allow matching menus (with fallback for Table Reservation naming variations)
|
||||||
|
if name in allowed_menus or name == 'Table Reservation':
|
||||||
|
# Hide Apps and Settings for non-admins
|
||||||
|
if name in ['Apps', 'Settings'] and not is_admin:
|
||||||
|
continue
|
||||||
|
new_children.append(child_id)
|
||||||
|
menus['root']['children'] = new_children
|
||||||
|
|
||||||
|
return menus
|
||||||
BIN
addons/dine360_dashboard/static/description/icon.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
176
addons/dine360_dashboard/static/src/css/home_menu.css
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
/* Main background */
|
||||||
|
body.o_home_dashboard,
|
||||||
|
#wrapwrap.o_home_dashboard,
|
||||||
|
.o_home_menu_background {
|
||||||
|
background: linear-gradient(rgb(0 0 0 / 83%), rgb(0 0 0 / 83%)),
|
||||||
|
url('/dine360_dashboard/static/src/img/dashboard_bg.png') no-repeat center center !important;
|
||||||
|
background-size: cover !important;
|
||||||
|
background-attachment: fixed !important;
|
||||||
|
min-height: 100vh !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
font-family: 'Inter', 'Segoe UI', Roboto, sans-serif !important;
|
||||||
|
position: relative !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#wrapwrap.o_home_dashboard #wrap {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_apps {
|
||||||
|
display: flex !important;
|
||||||
|
flex-wrap: wrap !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
gap: 40px 50px !important;
|
||||||
|
max-width: 1000px !important;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
padding: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_app {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
align-items: center !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) !important;
|
||||||
|
width: 120px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_app_icon_container {
|
||||||
|
background: #ffffff !important;
|
||||||
|
width: 90px !important;
|
||||||
|
height: 90px !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
border-radius: 24px !important;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 6px -1px rgba(0, 0, 0, 0.05),
|
||||||
|
0 2px 4px -1px rgba(0, 0, 0, 0.03) !important;
|
||||||
|
margin-bottom: 12px !important;
|
||||||
|
position: relative !important;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.03) !important;
|
||||||
|
transition: all 0.4s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_app:hover .o_app_icon_container {
|
||||||
|
transform: translateY(-10px) rotate(2deg) !important;
|
||||||
|
background: #ffffff !important;
|
||||||
|
border-bottom: 2px solid #d6111e !important;
|
||||||
|
box-shadow:
|
||||||
|
0 20px 25px -5px rgba(214, 17, 30, 0.2),
|
||||||
|
0 10px 10px -5px rgba(23, 20, 34, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_app_icon_container::before {
|
||||||
|
content: "";
|
||||||
|
--pos-accent-red: #d6111e;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(135deg, rgb(175 36 36 / 62%) 0%, rgb(181 84 84 / 20%) 50%);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_app_icon {
|
||||||
|
width: 56px !important;
|
||||||
|
height: 56px !important;
|
||||||
|
object-fit: contain !important;
|
||||||
|
z-index: 2;
|
||||||
|
transition: transform 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_app:hover .o_app_icon {
|
||||||
|
transform: scale(1.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_app_name {
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
text-align: center !important;
|
||||||
|
letter-spacing: -0.2px !important;
|
||||||
|
transition: color 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_app:hover .o_app_name {
|
||||||
|
color: #d6111e !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_home_top_bar {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 25px !important;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
z-index: 1000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_top_bar_island {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 12px !important;
|
||||||
|
padding: 10px 25px !important;
|
||||||
|
background: rgba(0, 0, 0, 0.9) !important;
|
||||||
|
backdrop-filter: blur(15px) !important;
|
||||||
|
-webkit-backdrop-filter: blur(15px);
|
||||||
|
border-radius: 50px !important;
|
||||||
|
border-color: #d6111e !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(214, 17, 30, 0.15) !important;
|
||||||
|
box-shadow: 0 12px 25px rgba(214, 17, 30, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_top_item {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #ffffff !important;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_top_item:hover {
|
||||||
|
color: #d6111e !important;
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_bar_divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 24px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_ai_icon {
|
||||||
|
font-weight: 800;
|
||||||
|
color: #d6111e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge_dot {
|
||||||
|
position: absolute !important;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: #d6111e !important;
|
||||||
|
border-radius: 50% !important;
|
||||||
|
border: 2px solid white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_user_avatar {
|
||||||
|
width: 35px !important;
|
||||||
|
height: 35px !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
background: #d6111e !important;
|
||||||
|
color: white !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
}
|
||||||
279
addons/dine360_dashboard/static/src/css/login_style.css
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
/* Container: Full Screen Split */
|
||||||
|
.o_login_main_wrapper {
|
||||||
|
display: flex !important;
|
||||||
|
height: 100vh !important;
|
||||||
|
width: 100vw !important;
|
||||||
|
overflow: hidden;
|
||||||
|
background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)), url('/dine360_theme_shivasakthi/static/src/img/chen-banner-2.webp') !important;
|
||||||
|
background-repeat: no-repeat !important;
|
||||||
|
background-position: center center !important;
|
||||||
|
background-size: cover !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Left Side: Background Image and Branding */
|
||||||
|
.o_login_left_side {
|
||||||
|
flex: 1.5 !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
padding: 60px !important;
|
||||||
|
position: relative;
|
||||||
|
/* Dark overlay on image for text readability */
|
||||||
|
/* background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)),
|
||||||
|
url('/dine360_dashboard/static/src/img/login_bg.png') no-repeat center center !important;
|
||||||
|
background-size: cover !important; */
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_login_content {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_login_content h1 {
|
||||||
|
font-size: 3.8rem !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-shadow: 2px 4px 15px rgba(0, 0, 0, 0.6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_login_content p {
|
||||||
|
font-size: 1.4rem !important;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Right Side: Form Section */
|
||||||
|
.o_login_right_side {
|
||||||
|
flex: 1 !important;
|
||||||
|
/* background: #ffffff !important; */
|
||||||
|
display: flex !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
align-items: center !important;
|
||||||
|
padding: 40px !important;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_login_card_wrapper {
|
||||||
|
width: 60%
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glassmorphism Card UI */
|
||||||
|
.o_login_card {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 450px !important;
|
||||||
|
padding: 40px !important;
|
||||||
|
background: rgba(255, 255, 255, 0.05) !important;
|
||||||
|
backdrop-filter: blur(15px) !important;
|
||||||
|
-webkit-backdrop-filter: blur(15px) !important;
|
||||||
|
border-radius: 20px !important;
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5) !important;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header Text Gradient */
|
||||||
|
.o_login_header_text h3 {
|
||||||
|
font-size: 2rem !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #d6111e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo */
|
||||||
|
.o_login_logo_container {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_login_logo_container img {
|
||||||
|
max-height: 120px;
|
||||||
|
/* Larger logo */
|
||||||
|
width: auto;
|
||||||
|
filter: drop-shadow(0 0 10px rgba(255, 255, 255, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Inputs */
|
||||||
|
.form-control:focus {
|
||||||
|
border-color: #FECD4F !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(254, 205, 79, 0.2) !important;
|
||||||
|
background: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_login_form_container label {
|
||||||
|
color: #ffffff !important;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_login_form_container a,
|
||||||
|
.o_login_form_container .btn-link {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
background: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3) !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
padding: 12px 15px !important;
|
||||||
|
height: auto !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Accessibility contrast for error messages */
|
||||||
|
.alert-danger {
|
||||||
|
background-color: rgba(220, 53, 69, 0.9) !important;
|
||||||
|
color: white !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Login Button with Gradient */
|
||||||
|
.oe_login_buttons .btn-primary {
|
||||||
|
background: #FECD4F !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
padding: 14px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
font-size: 1rem !important;
|
||||||
|
color: #04121D !important;
|
||||||
|
width: 100% !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
box-shadow: 0 4px 15px rgba(254, 205, 79, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_login_buttons .btn-primary:hover {
|
||||||
|
background: #04121D !important;
|
||||||
|
color: #FECD4F !important;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 20px rgba(254, 205, 79, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Responsive Handling */
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.o_login_main_wrapper {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_login_left_side {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_login_right_side {
|
||||||
|
background: #0f172a !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_login_card_wrapper {
|
||||||
|
width: 90% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_website_login_container .oe_login_form,
|
||||||
|
.oe_website_login_container .oe_signup_form,
|
||||||
|
.oe_website_login_container .oe_reset_password_form {
|
||||||
|
width: 90% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide Website Header/Footer ONLY on Login, Signup, Reset Password */
|
||||||
|
body.o_custom_login_body header,
|
||||||
|
body.o_custom_login_body footer,
|
||||||
|
body.o_custom_login_body .o_footer_copyright,
|
||||||
|
body.o_custom_login_body #o_main_nav,
|
||||||
|
body.o_custom_login_body .o_header_standard,
|
||||||
|
body.o_custom_login_body #wrapwrap>header,
|
||||||
|
body.o_custom_login_body #wrapwrap>footer {
|
||||||
|
display: none !important;
|
||||||
|
height: 0 !important;
|
||||||
|
width: 0 !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
position: absolute !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure wrapwrap doesn't have padding/margin from header on these pages */
|
||||||
|
body.o_custom_login_body #wrapwrap {
|
||||||
|
padding-top: 0 !important;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure the wrapper covers the entire screen, ignoring website container constraints */
|
||||||
|
body.o_custom_login_body .o_login_main_wrapper {
|
||||||
|
position: fixed !important;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw !important;
|
||||||
|
height: 100vh !important;
|
||||||
|
z-index: 9999;
|
||||||
|
/* Ensure it stays on top */
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Footer */
|
||||||
|
.o_login_footer_custom {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 50%;
|
||||||
|
/* Only on the right side which is 50% usually, or just right side */
|
||||||
|
text-align: center;
|
||||||
|
padding: 15px;
|
||||||
|
background: transparent;
|
||||||
|
color: #6c757d;
|
||||||
|
z-index: 10;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.o_login_footer_custom {
|
||||||
|
width: 100%;
|
||||||
|
color: white;
|
||||||
|
/* Visible on gradient background */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_login_footer_custom a {
|
||||||
|
color: #d6111e;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_login_footer_custom a:hover {
|
||||||
|
color: #171422;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* website login screen */
|
||||||
|
.oe_website_login_container {
|
||||||
|
display: flex !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
align-items: center !important;
|
||||||
|
height: 100vh !important;
|
||||||
|
/* width: 100vw !important; */
|
||||||
|
overflow: hidden !important;
|
||||||
|
background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)), url('/dine360_theme_shivasakthi/static/src/img/chen-banner-2.webp') no-repeat center center !important;
|
||||||
|
background-size: cover !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_website_login_container .oe_login_form,
|
||||||
|
.oe_website_login_container .oe_signup_form,
|
||||||
|
.oe_website_login_container .oe_reset_password_form {
|
||||||
|
width: 60% !important;
|
||||||
|
padding: 40px !important;
|
||||||
|
background: rgba(255, 255, 255, 0.05) !important;
|
||||||
|
backdrop-filter: blur(15px) !important;
|
||||||
|
-webkit-backdrop-filter: blur(15px) !important;
|
||||||
|
border-radius: 20px !important;
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5) !important;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||||||
|
z-index: 2;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
.oe_website_login_container label,
|
||||||
|
.oe_website_login_container a {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
446
addons/dine360_dashboard/static/src/css/pos_style.css
Normal file
@ -0,0 +1,446 @@
|
|||||||
|
/* ========================================
|
||||||
|
SHIVASAKTHI RUSH MODE - POS THEME (ULTRA PREMIUM)
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--pos-primary: #171422;
|
||||||
|
/* Secondary Dark */
|
||||||
|
--pos-secondary: #d6111e;
|
||||||
|
/* Main Teal */
|
||||||
|
--pos-accent-orange: #e66421;
|
||||||
|
/* Biryani Orange */
|
||||||
|
--pos-accent-gold: #cc9900;
|
||||||
|
/* Tandoor Gold */
|
||||||
|
--pos-accent-red: #d6111e;
|
||||||
|
/* Main Teal */
|
||||||
|
--pos-bg-main: #f8fafc;
|
||||||
|
/* Ultra light gray */
|
||||||
|
--pos-card-dark: #1a1d23;
|
||||||
|
/* Dark Card Background */
|
||||||
|
--pos-text-light: #ffffff;
|
||||||
|
--pos-text-dark: #111827;
|
||||||
|
--border-radius-xl: 24px;
|
||||||
|
--border-radius-lg: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 1. Main POS Layout Swapping */
|
||||||
|
/* Standard Odoo: Leftpane = Cart, Rightpane = Products */
|
||||||
|
/* Goal: Food List on Left, Orders/Calculations on Right */
|
||||||
|
/* .pos .pos-content,
|
||||||
|
.pos .product-screen {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: row-reverse !important; */
|
||||||
|
/* SWAP Panes */
|
||||||
|
/* height: calc(100vh - 85px) !important;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.pos .leftpane {
|
||||||
|
/* The Cart Section - Now on Right */
|
||||||
|
width: 480px !important;
|
||||||
|
border-left: 2px solid #f1f5f9 !important;
|
||||||
|
border-right: none !important;
|
||||||
|
background: #ffffff !important;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
flex: none !important;
|
||||||
|
height: calc(100vh - 85px) !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
z-index: 100 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .order-container {
|
||||||
|
flex: 1 1 auto !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
background: #ffffff !important;
|
||||||
|
min-height: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-screen {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: row-reverse !important;
|
||||||
|
height: calc(100vh - 85px) !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .rightpane {
|
||||||
|
/* The Product Grid Section - Now on Left */
|
||||||
|
flex: 1 1 auto !important;
|
||||||
|
background: #f8fafc !important;
|
||||||
|
height: 100% !important;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .product-list-container {
|
||||||
|
flex: 1 1 auto !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
background: #f8fafc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2. Header & Branding */
|
||||||
|
.pos .pos-topheader {
|
||||||
|
background: #171422 !important;
|
||||||
|
border-bottom: 2px solid #d6111e !important;
|
||||||
|
color: #fff !important;
|
||||||
|
height: 85px !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
padding: 0 30px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo from Odoo Settings */
|
||||||
|
.pos .pos-logo {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 15px !important;
|
||||||
|
margin-right: 50px !important;
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .pos-logo img,
|
||||||
|
.pos .pos-logo {
|
||||||
|
content: url('/web/binary/company_logo') !important;
|
||||||
|
height: 55px !important;
|
||||||
|
width: auto !important;
|
||||||
|
object-fit: contain !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "Rush Mode" Label removed for production */
|
||||||
|
|
||||||
|
/* 3. Search Bar - Teal Theme */
|
||||||
|
.pos .search-bar {
|
||||||
|
background: #e0f2f1 !important;
|
||||||
|
border-radius: var(--border-radius-lg) !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
width: 380px !important;
|
||||||
|
height: 50px !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
padding: 0 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .search-bar input {
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
color: #00695c !important;
|
||||||
|
font-size: 16px !important;
|
||||||
|
text-transform: uppercase !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 4. Category Bar - Fix Truncation */
|
||||||
|
.pos .category-list {
|
||||||
|
display: flex !important;
|
||||||
|
gap: 12px !important;
|
||||||
|
padding: 15px 30px !important;
|
||||||
|
background: #ffffff !important;
|
||||||
|
border-bottom: 2px solid #f1f5f9 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .category-button {
|
||||||
|
border-radius: var(--border-radius-lg) !important;
|
||||||
|
font-weight: 900 !important;
|
||||||
|
text-transform: uppercase !important;
|
||||||
|
padding: 12px 30px !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
border: none !important;
|
||||||
|
min-width: 140px !important;
|
||||||
|
/* STOP MAI... truncation */
|
||||||
|
white-space: nowrap !important;
|
||||||
|
background: #f1f5f9 !important;
|
||||||
|
color: #475569 !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .category-button.active {
|
||||||
|
background: #d6111e !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dynamic Colors for specific indexes */
|
||||||
|
.pos .category-button:nth-child(2) {
|
||||||
|
background: var(--pos-accent-orange) !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .category-button:nth-child(3) {
|
||||||
|
background: var(--pos-accent-gold) !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 5. Product Cards - Dark Premium */
|
||||||
|
.pos .product-list-container {
|
||||||
|
background: #f8fafc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .product-list {
|
||||||
|
display: flex !important;
|
||||||
|
flex-wrap: wrap !important;
|
||||||
|
padding: 25px !important;
|
||||||
|
gap: 25px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .product {
|
||||||
|
background: #171422 !important;
|
||||||
|
border-radius: var(--border-radius-lg) !important;
|
||||||
|
width: 210px !important;
|
||||||
|
height: 230px !important;
|
||||||
|
flex: 0 0 auto !important;
|
||||||
|
flex-shrink: 0 !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(42, 106, 126, 0.05) !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
position: relative !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .product:hover {
|
||||||
|
transform: translateY(-8px) scale(1.02) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .product .product-img {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 150px !important;
|
||||||
|
object-fit: cover !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-img img {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
/* object-fit: cover !important; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .product .product-name {
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-size: 16px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
padding: 12px 15px 5px 15px !important;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .product .price-tag {
|
||||||
|
background: transparent !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-size: 15px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
position: relative !important;
|
||||||
|
bottom: 5px !important;
|
||||||
|
left: 15px !important;
|
||||||
|
top: auto !important;
|
||||||
|
right: auto !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 6. Order (Cart) Styling - Now on Right */
|
||||||
|
.pos .order-container {
|
||||||
|
background: #ffffff !important;
|
||||||
|
padding: 10px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .orderline {
|
||||||
|
border-radius: var(--border-radius-lg) !important;
|
||||||
|
border: 2px solid #f1f5f9 !important;
|
||||||
|
margin: 8px 20px !important;
|
||||||
|
padding: 15px !important;
|
||||||
|
background: white !important;
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .orderline.selected {
|
||||||
|
border: 2px solid var(--pos-secondary) !important;
|
||||||
|
background: #f0fdf4 !important;
|
||||||
|
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .orderline .product-name {
|
||||||
|
font-weight: 900 !important;
|
||||||
|
font-size: 17px !important;
|
||||||
|
color: #111827 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 7. Summary & Pay Button */
|
||||||
|
.pos .order-summary {
|
||||||
|
background: #ffffff !important;
|
||||||
|
padding: 25px 30px !important;
|
||||||
|
border-top: 2px dashed #e2e8f0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .summary .total {
|
||||||
|
color: var(--pos-secondary) !important;
|
||||||
|
font-size: 40px !important;
|
||||||
|
font-weight: 900 !important;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .action-pad .button.pay {
|
||||||
|
background: var(--pos-secondary) !important;
|
||||||
|
color: white !important;
|
||||||
|
border-radius: var(--border-radius-lg) !important;
|
||||||
|
margin: 20px !important;
|
||||||
|
height: 75px !important;
|
||||||
|
font-size: 24px !important;
|
||||||
|
font-weight: 900 !important;
|
||||||
|
text-transform: uppercase !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
box-shadow: 0 12px 25px rgba(214, 17, 30, 0.3) !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .action-pad .button.pay::after {
|
||||||
|
content: "\2794";
|
||||||
|
margin-left: 15px;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 8. Numpad styling */
|
||||||
|
.pos .numpad {
|
||||||
|
background: #f8fafc !important;
|
||||||
|
padding: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .numpad button {
|
||||||
|
border-radius: 12px !important;
|
||||||
|
border: 1px solid #e2e8f0 !important;
|
||||||
|
background: white !important;
|
||||||
|
height: 55px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 9. Receipt Screen & New Order Button - Ultra Aggressive Theme Match */
|
||||||
|
.pos .button.next,
|
||||||
|
.pos .button.validation,
|
||||||
|
.pos .receipt-screen .button.next,
|
||||||
|
.pos .receipt-screen .validation.button,
|
||||||
|
.pos .receipt-screen .button {
|
||||||
|
background: #d61112 !important;
|
||||||
|
color: white !important;
|
||||||
|
border-radius: var(--border-radius-lg) !important;
|
||||||
|
height: 70px !important;
|
||||||
|
font-size: 22px !important;
|
||||||
|
font-weight: 900 !important;
|
||||||
|
text-transform: uppercase !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
box-shadow: 0 10px 20px rgba(214, 17, 18, 0.2) !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .receipt-screen .button.next:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 10. Premium Bill UI (Right Side) */
|
||||||
|
.pos-receipt {
|
||||||
|
font-family: 'Inter', sans-serif !important;
|
||||||
|
color: #1a1d23 !important;
|
||||||
|
padding: 30px !important;
|
||||||
|
background: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-receipt .pos-receipt-contact {
|
||||||
|
font-size: 13px !important;
|
||||||
|
color: #64748b !important;
|
||||||
|
margin-bottom: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-receipt .pos-receipt-center-align {
|
||||||
|
text-align: center !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-receipt .pos-receipt-order-data {
|
||||||
|
color: #94a3b8 !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
margin-top: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-receipt .receipt-orderlines {
|
||||||
|
border-top: 2px solid #f1f5f9 !important;
|
||||||
|
padding-top: 15px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-receipt .orderline {
|
||||||
|
border: none !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 8px 0 !important;
|
||||||
|
border-bottom: 1px dashed #e2e8f0 !important;
|
||||||
|
display: flex !important;
|
||||||
|
flex-wrap: wrap !important;
|
||||||
|
justify-content: space-between !important;
|
||||||
|
align-items: baseline !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-receipt .orderline .product-name {
|
||||||
|
flex: 1 1 65% !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
white-space: normal !important;
|
||||||
|
word-break: break-word !important;
|
||||||
|
padding-right: 5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-receipt .orderline .pos-receipt-right-align {
|
||||||
|
flex: 0 0 auto !important;
|
||||||
|
text-align: right !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-receipt .pos-receipt-total {
|
||||||
|
font-size: 24px !important;
|
||||||
|
font-weight: 900 !important;
|
||||||
|
color: var(--pos-secondary) !important;
|
||||||
|
border-top: 2px solid #1a1d23 !important;
|
||||||
|
padding-top: 15px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-receipt-amount {
|
||||||
|
font-weight: 800 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
RESPONSIVE MEDIA QUERIES
|
||||||
|
======================================== */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.pos .leftpane {
|
||||||
|
width: 380px !important;
|
||||||
|
}
|
||||||
|
.pos .product {
|
||||||
|
width: 160px !important;
|
||||||
|
height: 180px !important;
|
||||||
|
}
|
||||||
|
.pos .product .product-img {
|
||||||
|
height: 110px !important;
|
||||||
|
}
|
||||||
|
.pos .search-bar {
|
||||||
|
width: 280px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.product-screen {
|
||||||
|
flex-direction: column !important;
|
||||||
|
height: 100vh !important;
|
||||||
|
}
|
||||||
|
.pos .leftpane {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 50vh !important;
|
||||||
|
border-left: none !important;
|
||||||
|
border-top: 2px solid #f1f5f9 !important;
|
||||||
|
}
|
||||||
|
.pos .rightpane {
|
||||||
|
height: 50vh !important;
|
||||||
|
}
|
||||||
|
.pos .pos-topheader {
|
||||||
|
height: auto !important;
|
||||||
|
flex-wrap: wrap !important;
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
.pos .search-bar {
|
||||||
|
width: 100% !important;
|
||||||
|
margin-top: 10px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
312
addons/dine360_dashboard/static/src/css/shop_style.css
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
/* ========================================
|
||||||
|
SHOP PAGE - LEFT SIDEBAR FILTER STYLING
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
/* Ensure Shop Page Layout with Sidebar */
|
||||||
|
#wrapwrap.oe_website_sale {
|
||||||
|
background: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Shop Container - Two Column Layout */
|
||||||
|
.oe_website_sale #products_grid_before {
|
||||||
|
display: block !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Left Sidebar Filter Section */
|
||||||
|
.oe_website_sale #o_shop_collapse_category,
|
||||||
|
.oe_website_sale .o_wsale_products_searchbar_form,
|
||||||
|
.oe_website_sale #wsale_products_categories_collapse {
|
||||||
|
display: block !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shop Layout Container */
|
||||||
|
.oe_website_sale .container,
|
||||||
|
.oe_website_sale .container-fluid {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Row with Sidebar and Products */
|
||||||
|
.oe_website_sale .row {
|
||||||
|
display: flex !important;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Left Sidebar Column (Categories/Filters) */
|
||||||
|
.oe_website_sale #products_grid_before,
|
||||||
|
.oe_website_sale .col-lg-3,
|
||||||
|
.oe_website_sale aside {
|
||||||
|
flex: 0 0 280px !important;
|
||||||
|
max-width: 280px !important;
|
||||||
|
display: block !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Right Products Column */
|
||||||
|
.oe_website_sale #products_grid,
|
||||||
|
.oe_website_sale .col-lg-9 {
|
||||||
|
flex: 1 !important;
|
||||||
|
max-width: calc(100% - 280px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter Sidebar Styling */
|
||||||
|
.oe_website_sale #products_grid_before {
|
||||||
|
background: rgba(255, 255, 255, 0.95) !important;
|
||||||
|
border-radius: 16px !important;
|
||||||
|
padding: 20px !important;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08) !important;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Category Filter Section */
|
||||||
|
.oe_website_sale .o_wsale_products_searchbar_form,
|
||||||
|
.oe_website_sale #wsale_products_categories_collapse {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Category Filter Title */
|
||||||
|
.oe_website_sale #products_grid_before h5,
|
||||||
|
.oe_website_sale #products_grid_before .h5,
|
||||||
|
.oe_website_sale #products_grid_before h6 {
|
||||||
|
color: var(--secondary-color, #2BB1A5) !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
font-size: 1.1rem !important;
|
||||||
|
margin-bottom: 15px !important;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Category List Items */
|
||||||
|
.oe_website_sale #products_grid_before ul,
|
||||||
|
.oe_website_sale #products_grid_before .list-group {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_website_sale #products_grid_before li,
|
||||||
|
.oe_website_sale #products_grid_before .list-group-item {
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
padding: 10px 15px !important;
|
||||||
|
margin-bottom: 5px !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_website_sale #products_grid_before li a,
|
||||||
|
.oe_website_sale #products_grid_before .list-group-item a {
|
||||||
|
color: #4a5568 !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_website_sale #products_grid_before li:hover,
|
||||||
|
.oe_website_sale #products_grid_before .list-group-item:hover {
|
||||||
|
background: #fecd4f !important;
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_website_sale #products_grid_before li:hover a,
|
||||||
|
.oe_website_sale #products_grid_before .list-group-item:hover a {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active Category */
|
||||||
|
.oe_website_sale #products_grid_before li.active,
|
||||||
|
.oe_website_sale #products_grid_before .list-group-item.active {
|
||||||
|
background: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_website_sale #products_grid_before li.active a,
|
||||||
|
.oe_website_sale #products_grid_before .list-group-item.active a {
|
||||||
|
color: #000000 !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search Bar in Sidebar */
|
||||||
|
.oe_website_sale #products_grid_before .form-control,
|
||||||
|
.oe_website_sale #products_grid_before input[type="text"],
|
||||||
|
.oe_website_sale #products_grid_before input[type="search"] {
|
||||||
|
border-radius: 10px !important;
|
||||||
|
border: 1px solid #e2e8f0 !important;
|
||||||
|
padding: 10px 15px !important;
|
||||||
|
background: #f8f9fa !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_website_sale #products_grid_before .form-control:focus,
|
||||||
|
.oe_website_sale #products_grid_before input:focus {
|
||||||
|
border-color: var(--secondary-color, #2BB1A5) !important;
|
||||||
|
background: #ffffff !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(43, 177, 165, 0.15) !important;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter Buttons */
|
||||||
|
.oe_website_sale #products_grid_before .btn,
|
||||||
|
.oe_website_sale #products_grid_before button {
|
||||||
|
border-radius: 10px !important;
|
||||||
|
padding: 8px 20px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_website_sale #products_grid_before .btn-primary {
|
||||||
|
background: #2BB1A5 !important;
|
||||||
|
border: none !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
box-shadow: 0 4px 12px rgba(43, 177, 165, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_website_sale #products_grid_before .btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 15px rgba(43, 177, 165, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Attribute Filters (Color, Size, etc.) */
|
||||||
|
.oe_website_sale #products_grid_before .css_attribute_color {
|
||||||
|
display: inline-block;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 5px;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_website_sale #products_grid_before .css_attribute_color:hover,
|
||||||
|
.oe_website_sale #products_grid_before .css_attribute_color.active {
|
||||||
|
border-color: var(--secondary-color, #2BB1A5);
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 2px 8px rgba(43, 177, 165, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Price Range Filter */
|
||||||
|
.oe_website_sale #products_grid_before .o_wsale_price_filter {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_website_sale #products_grid_before .o_wsale_price_filter input[type="range"] {
|
||||||
|
width: 100%;
|
||||||
|
accent-color: var(--secondary-color, #2BB1A5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collapse/Expand Buttons */
|
||||||
|
.oe_website_sale #products_grid_before .btn-link {
|
||||||
|
color: var(--secondary-color, #2BB1A5) !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_website_sale #products_grid_before .btn-link:hover {
|
||||||
|
color: var(--primary-color, #171422) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive */
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
|
||||||
|
.oe_website_sale #products_grid_before,
|
||||||
|
.oe_website_sale .col-lg-3,
|
||||||
|
.oe_website_sale aside {
|
||||||
|
flex: 0 0 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_website_sale #products_grid,
|
||||||
|
.oe_website_sale .col-lg-9 {
|
||||||
|
flex: 0 0 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure visibility of all filter elements */
|
||||||
|
.oe_website_sale .o_wsale_products_searchbar_form,
|
||||||
|
.oe_website_sale .o_wsale_products_categories,
|
||||||
|
.oe_website_sale #wsale_products_categories_collapse,
|
||||||
|
.oe_website_sale .o_wsale_products_attributes,
|
||||||
|
.oe_website_sale .o_wsale_products_price {
|
||||||
|
display: block !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Category Navigation */
|
||||||
|
.oe_website_sale .o_wsale_products_categories ul.o_wsale_categories_list {
|
||||||
|
padding-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_website_sale .o_wsale_products_categories ul.o_wsale_categories_list li {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Attribute Filters Section */
|
||||||
|
.oe_website_sale .o_wsale_products_attributes {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_website_sale .o_wsale_products_attributes .o_wsale_attribute {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_website_sale .o_wsale_products_attributes .o_wsale_attribute label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #4a5568;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clear Filters Button */
|
||||||
|
.oe_website_sale .o_wsale_clear_filters {
|
||||||
|
margin-top: 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_website_sale .o_wsale_clear_filters .btn {
|
||||||
|
width: 100%;
|
||||||
|
background: #f8f9fa !important;
|
||||||
|
border: 1px solid #e2e8f0 !important;
|
||||||
|
color: #4a5568 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oe_website_sale .o_wsale_clear_filters .btn:hover {
|
||||||
|
background: #ffffff !important;
|
||||||
|
border-color: var(--secondary-color, #2BB1A5) !important;
|
||||||
|
color: var(--secondary-color, #2BB1A5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.css_quantity input {
|
||||||
|
max-width: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Product Details Quantity Input Fix */
|
||||||
|
.css_quantity input.quantity {
|
||||||
|
color: #000000 !important;
|
||||||
|
background-color: #ffffff !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
width: 60px !important;
|
||||||
|
max-width: 60px !important;
|
||||||
|
height: 45px !important;
|
||||||
|
line-height: 45px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
font-size: 18px !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
text-align: center !important;
|
||||||
|
border: 1px solid #000000 !important;
|
||||||
|
display: inline-block !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
599
addons/dine360_dashboard/static/src/css/theme_variables.css
Normal file
@ -0,0 +1,599 @@
|
|||||||
|
/* ========================================
|
||||||
|
ODOO THEME COLOR VARIABLES
|
||||||
|
Primary Color: #ec0000 (Dine360 Red)
|
||||||
|
Secondary Color: #000000 (Black)
|
||||||
|
======================================== */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Primary Brand Colors */
|
||||||
|
--primary-color: #d6111e;
|
||||||
|
--primary-dark: #8B0A0E;
|
||||||
|
--primary-light: #E57373;
|
||||||
|
--primary-lighter: #FFCDD2;
|
||||||
|
|
||||||
|
/* Secondary/Accent Color */
|
||||||
|
--secondary-color: #171422;
|
||||||
|
--secondary-dark: #000000;
|
||||||
|
--secondary-light: #2d293d;
|
||||||
|
|
||||||
|
/* Gradient Combinations Removed - Using Solid Colors */
|
||||||
|
--gradient-primary: #d6111e;
|
||||||
|
--gradient-primary-hover: #8B0A0E;
|
||||||
|
|
||||||
|
/* Home Page Background */
|
||||||
|
--bg-gradient-main: #ffffff;
|
||||||
|
|
||||||
|
/* UI Element Colors */
|
||||||
|
--btn-primary-bg: #d6111e;
|
||||||
|
--link-color: #171422;
|
||||||
|
--link-hover: #d6111e;
|
||||||
|
|
||||||
|
/* Status Colors */
|
||||||
|
--success-color: #171422;
|
||||||
|
--warning-color: #FECD4F;
|
||||||
|
--danger-color: #d6111e;
|
||||||
|
--info-color: #2d293d;
|
||||||
|
|
||||||
|
/* Shadows */
|
||||||
|
--shadow-primary: 0 4px 15px rgba(214, 17, 30, 0.2);
|
||||||
|
--shadow-primary-hover: 0 8px 20px rgba(214, 17, 30, 0.3);
|
||||||
|
--shadow-card: 0 10px 30px rgba(23, 20, 34, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ::selection - Text Selection Color */
|
||||||
|
::selection {
|
||||||
|
background: var(--primary-color) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar Styling (Webkit) */
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--secondary-color) !important;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------
|
||||||
|
GLOBAL UI OVERRIDES (HOME PAGE STYLE)
|
||||||
|
--------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* Global Background & Font */
|
||||||
|
body,
|
||||||
|
.o_web_client {
|
||||||
|
background: var(--bg-gradient-main) !important;
|
||||||
|
font-family: 'Inter', 'Segoe UI', Roboto, sans-serif !important;
|
||||||
|
font-size: 15px !important;
|
||||||
|
/* Increased Font Size for Readability */
|
||||||
|
background-attachment: fixed !important;
|
||||||
|
background-size: cover !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
REMOVED AGGRESSIVE LAYOUT OVERRIDES
|
||||||
|
(o_action_manager, o_view_controller overrides deleted to restore Odoo default structural behavior)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------
|
||||||
|
NAVBAR & SIDEBAR (CONTROL PANEL)
|
||||||
|
--------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* Top Navbar - Glassmorphism */
|
||||||
|
.o_main_navbar {
|
||||||
|
background: #ffffff !important;
|
||||||
|
border-bottom: 2px solid var(--primary-color) !important;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.03) !important;
|
||||||
|
color: #2c3e50 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_main_navbar .o_menu_brand {
|
||||||
|
color: var(--secondary-color) !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
font-size: 1.4rem !important;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_main_navbar .o_menu_sections .o_nav_entry,
|
||||||
|
.o_main_navbar .o_menu_systray .o_nav_entry {
|
||||||
|
color: #111111 !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_main_navbar .o_menu_sections .o_nav_entry:hover,
|
||||||
|
.o_main_navbar .o_menu_systray .o_nav_entry:hover {
|
||||||
|
background: var(--primary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Control Panel (Search, Filter Bar) */
|
||||||
|
.o_control_panel {
|
||||||
|
background: #fdfdfd !important;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Breadcrumbs */
|
||||||
|
.breadcrumb-item a {
|
||||||
|
color: #FECD4F !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
font-size: 18px !important;
|
||||||
|
/* Larger headers */
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item.active {
|
||||||
|
color: #718096 !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
font-size: 18px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search Bar Input */
|
||||||
|
.o_searchview {
|
||||||
|
background: #f8f9fa !important;
|
||||||
|
border: 1px solid #e2e8f0 !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
padding: 6px 15px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_searchview .o_searchview_input {
|
||||||
|
background: transparent !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------
|
||||||
|
TABLES (LIST VIEW)
|
||||||
|
--------------------------------------------------------- */
|
||||||
|
|
||||||
|
.o_list_view .o_list_table {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Header */
|
||||||
|
.o_list_view .o_list_table thead {
|
||||||
|
background: rgba(248, 249, 250, 0.8) !important;
|
||||||
|
color: #4a5568 !important;
|
||||||
|
border-bottom: 2px solid var(--secondary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_list_view .o_list_table thead th {
|
||||||
|
border: none !important;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
padding: 12px 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table Rows */
|
||||||
|
.o_list_view .o_list_table tbody tr.o_data_row {
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.03) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_list_view .o_list_table tbody tr.o_data_row:hover {
|
||||||
|
background-color: rgba(214, 17, 30, 0.05) !important;
|
||||||
|
/* Light Red Hover */
|
||||||
|
transform: scale(1.002);
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------
|
||||||
|
KANBAN CARDS
|
||||||
|
--------------------------------------------------------- */
|
||||||
|
|
||||||
|
.o_kanban_view .o_kanban_record {
|
||||||
|
background: #fff !important;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.04) !important;
|
||||||
|
border-radius: 16px !important;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.02) !important;
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover Effect for Cards */
|
||||||
|
.o_kanban_view .o_kanban_record:hover {
|
||||||
|
transform: translateY(-5px) !important;
|
||||||
|
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.08) !important;
|
||||||
|
border-color: var(--primary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Header/Title */
|
||||||
|
.o_kanban_record .o_kanban_record_title,
|
||||||
|
.o_kanban_record strong {
|
||||||
|
color: #2d3748 !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
font-size: 1.05rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kanban Group Headers */
|
||||||
|
.o_kanban_view .o_kanban_group_header {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------
|
||||||
|
BUTTONS & FORM ELEMENTS
|
||||||
|
--------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* Gradient Buttons */
|
||||||
|
.btn-primary,
|
||||||
|
.o_form_button_save,
|
||||||
|
.o_form_button_create {
|
||||||
|
background: var(--gradient-primary) !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
padding: 8px 20px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
box-shadow: 0 4px 12px rgba(214, 17, 30, 0.3) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover,
|
||||||
|
.o_form_button_save:hover,
|
||||||
|
.o_form_button_create:hover {
|
||||||
|
background: var(--gradient-primary-hover) !important;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 15px rgba(214, 17, 30, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Secondary Buttons */
|
||||||
|
.btn-secondary {
|
||||||
|
background: #fff !important;
|
||||||
|
border: 1px solid #e2e8f0 !important;
|
||||||
|
color: #4a5568 !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #f7fafc !important;
|
||||||
|
color: var(--secondary-color) !important;
|
||||||
|
border-color: var(--secondary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Inputs */
|
||||||
|
.o_form_view .o_input {
|
||||||
|
border-radius: 8px !important;
|
||||||
|
border: 1px solid #e2e8f0 !important;
|
||||||
|
padding: 8px 12px !important;
|
||||||
|
background-color: #fcfcfc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_form_view .o_input:focus {
|
||||||
|
border-color: var(--secondary-color) !important;
|
||||||
|
background-color: #fff !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(214, 17, 30, 0.15) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------
|
||||||
|
COMPONENT OVERRIDES
|
||||||
|
--------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* Primary Buttons */
|
||||||
|
.btn-primary,
|
||||||
|
.o_form_button_save,
|
||||||
|
.o_form_button_create,
|
||||||
|
.o_button_import {
|
||||||
|
background: var(--gradient-primary) !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
padding: 8px 20px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
box-shadow: 0 4px 12px rgba(214, 17, 30, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover,
|
||||||
|
.o_form_button_save:hover,
|
||||||
|
.o_form_button_create:hover,
|
||||||
|
.o_button_import:hover {
|
||||||
|
background: var(--gradient-primary-hover) !important;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 15px rgba(214, 17, 30, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------
|
||||||
|
DASHBOARD SPECIFIC UI UPDATES (But safer for global)
|
||||||
|
--------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* Top Menu Tabs */
|
||||||
|
.o_main_navbar .o_menu_sections .dropdown-item,
|
||||||
|
.o_main_navbar .o_menu_sections .o_nav_entry {
|
||||||
|
background: transparent !important;
|
||||||
|
color: var(--primary-color) !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
margin: 0 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active Tab / Hover State */
|
||||||
|
.o_main_navbar .o_menu_sections .o_nav_entry:hover,
|
||||||
|
.o_main_navbar .o_menu_sections .show .dropdown-toggle {
|
||||||
|
background-color: #ffffff !important;
|
||||||
|
/* Yellow Active Background */
|
||||||
|
color: #d6111e !important;
|
||||||
|
border-bottom: 2px solid #d6111e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Sidebar (Finance/Logistics) */
|
||||||
|
.o_notebook .nav.nav-tabs,
|
||||||
|
.o_dashboard_view .o_group_selector {
|
||||||
|
background: rgba(255, 255, 255, 0.6) !important;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-right: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 15px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_notebook .nav-link,
|
||||||
|
.o_dashboard_view .o_group_selector li {
|
||||||
|
color: var(--secondary-color) !important;
|
||||||
|
/* Teal Text */
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
margin-bottom: 8px !important;
|
||||||
|
padding: 10px 15px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_notebook .nav-link.active,
|
||||||
|
.o_dashboard_view .o_group_selector li.selected {
|
||||||
|
background: var(--gradient-primary) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
box-shadow: 0 4px 10px rgba(214, 17, 30, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard KPI Cards (Invoiced, Average Invoice, DSO) */
|
||||||
|
.o_dashboard_view .o_group .o_aggregate,
|
||||||
|
.o_group .o_group_col_6 {
|
||||||
|
background: #fff !important;
|
||||||
|
border-radius: 15px !important;
|
||||||
|
padding: 20px !important;
|
||||||
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.05) !important;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.02) !important;
|
||||||
|
transition: transform 0.3s ease !important;
|
||||||
|
margin-bottom: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_dashboard_view .o_group .o_aggregate:hover {
|
||||||
|
transform: translateY(-5px) !important;
|
||||||
|
box-shadow: 0 10px 25px rgba(214, 17, 30, 0.15) !important;
|
||||||
|
border-color: var(--secondary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Value Text in Cards */
|
||||||
|
.o_dashboard_view .o_group .o_aggregate .o_value {
|
||||||
|
color: var(--secondary-color) !important;
|
||||||
|
font-weight: 800 !important;
|
||||||
|
font-size: 2rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_dashboard_view .o_group .o_aggregate label {
|
||||||
|
color: #718096 !important;
|
||||||
|
font-size: 0.9rem !important;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Charts Background */
|
||||||
|
.o_graph_view {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_graph_canvas_container canvas {
|
||||||
|
background: rgba(255, 255, 255, 0.5) !important;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links */
|
||||||
|
a,
|
||||||
|
.o_form_uri,
|
||||||
|
.oe_link,
|
||||||
|
.btn-link {
|
||||||
|
color: var(--secondary-color) !important;
|
||||||
|
/* Switched to teal for better readibility on white */
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover,
|
||||||
|
.o_form_uri:hover,
|
||||||
|
.oe_link:hover,
|
||||||
|
.btn-link:hover {
|
||||||
|
color: var(--primary-dark) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nav & Menu Active States */
|
||||||
|
.nav-link.active,
|
||||||
|
.o_menu_sections .dropdown-item.active,
|
||||||
|
.o_menu_brand:hover {
|
||||||
|
color: var(--secondary-color) !important;
|
||||||
|
border-bottom-color: var(--secondary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badges */
|
||||||
|
.badge-primary,
|
||||||
|
.o_tag {
|
||||||
|
background-color: var(--primary-color) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress bars */
|
||||||
|
.progress-bar {
|
||||||
|
background-color: var(--secondary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkboxes and Radios */
|
||||||
|
.custom-control-input:checked~.custom-control-label::before,
|
||||||
|
.o_checkbox input:checked+label::before {
|
||||||
|
background-color: var(--secondary-color) !important;
|
||||||
|
border-color: var(--secondary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Controls - Focus using Secondary (Teal) like Login Page */
|
||||||
|
.form-control:focus,
|
||||||
|
.o_input:focus,
|
||||||
|
.o_searchview_input:focus {
|
||||||
|
border-color: var(--secondary-color) !important;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(214, 17, 30, 0.25) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading Bar */
|
||||||
|
.o_loading {
|
||||||
|
background-color: var(--primary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Backend View Headers */
|
||||||
|
.o_list_view .o_list_table thead,
|
||||||
|
.o_kanban_view .o_kanban_group_header {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-bottom: 2px solid var(--primary-color) !important;
|
||||||
|
color: #4a5568 !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kanban Records Selected */
|
||||||
|
.o_kanban_record.o_kanban_record_selected {
|
||||||
|
box-shadow: 0 0 0 2px var(--primary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pagination */
|
||||||
|
.page-item.active .page-link {
|
||||||
|
background-color: var(--secondary-color) !important;
|
||||||
|
border-color: var(--secondary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Switcher/Toggle */
|
||||||
|
.o_switch.custom-switch .custom-control-input:checked~.custom-control-label::before {
|
||||||
|
background-color: var(--secondary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar Today */
|
||||||
|
.fc-day-today {
|
||||||
|
background-color: rgba(254, 205, 79, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------
|
||||||
|
HEADER SUBMENU DROPDOWN NAVIGATION
|
||||||
|
--------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* Dropdown Container */
|
||||||
|
.dropdown-menu {
|
||||||
|
background: #ffffff !important;
|
||||||
|
border: 1px solid #e2e8f0 !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.15) !important;
|
||||||
|
padding: 8px !important;
|
||||||
|
margin-top: 5px !important;
|
||||||
|
z-index: 10000 !important;
|
||||||
|
/* Raised to be above most backend elements */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix for Dropdowns being hidden in List Views (Editable rows) */
|
||||||
|
.o_list_view .o_list_table {
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_list_view .o_list_table tbody tr,
|
||||||
|
.o_list_view .o_list_table tbody td {
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure Autocomplete stays in front */
|
||||||
|
.ui-autocomplete,
|
||||||
|
.o_autocomplete_dropdown,
|
||||||
|
.dropdown-menu.show {
|
||||||
|
z-index: 10001 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.o_main_navbar .o_menu_sections .o_nav_entry.active,
|
||||||
|
.o_main_navbar .o_menu_sections .dropdown-toggle {
|
||||||
|
background-color: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.o_main_navbar .o_menu_sections .o_nav_entry.active,
|
||||||
|
.o_main_navbar .o_menu_sections .dropdown-toggle.active {
|
||||||
|
background-color: var(--primary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_main_navbar .o_menu_brand,
|
||||||
|
.o_main_navbar .o_navbar_apps_menu .dropdown-toggle,
|
||||||
|
.o_main_navbar .o_nav_entry,
|
||||||
|
.o_main_navbar .dropdown-toggle {
|
||||||
|
color: var(--secondary-color) !important;
|
||||||
|
font-size: 1.1rem !important;
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navbar Icons */
|
||||||
|
.o_main_navbar .o_menu_systray .o_nav_entry i,
|
||||||
|
.o_main_navbar .o_menu_apps .o_app_drawer i,
|
||||||
|
.o_main_navbar .o_menu_sections .fa {
|
||||||
|
color: var(--secondary-color) !important;
|
||||||
|
font-size: 1.1rem !important;
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_main_navbar .o_menu_systray .o_nav_entry:hover i,
|
||||||
|
.o_main_navbar .o_menu_apps .o_app_drawer:hover i {
|
||||||
|
color: var(--primary-dark) !important;
|
||||||
|
transform: scale(1.1);
|
||||||
|
text-shadow: 0 2px 10px rgba(214, 17, 30, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown Items */
|
||||||
|
.dropdown-item {
|
||||||
|
color: #4a5568 !important;
|
||||||
|
padding: 10px 15px !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
|
margin-bottom: 2px !important;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown Item Hover */
|
||||||
|
.dropdown-item:hover,
|
||||||
|
.dropdown-item:focus {
|
||||||
|
background: var(--gradient-primary) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
box-shadow: 0 4px 10px rgba(214, 17, 30, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown Header/Divider */
|
||||||
|
.dropdown-divider {
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.06) !important;
|
||||||
|
margin: 8px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-header {
|
||||||
|
color: var(--secondary-color) !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.75rem !important;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
padding: 8px 15px !important;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active Dropdown Item */
|
||||||
|
.dropdown-item.active,
|
||||||
|
.dropdown-item:active {
|
||||||
|
background: var(--gradient-primary) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_main_navbar .o_menu_sections .o_nav_entry,
|
||||||
|
.o_main_navbar .o_menu_sections .dropdown-toggle {
|
||||||
|
background: #ffffff;
|
||||||
|
border-bottom: 2px solid #d6111e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item.active {
|
||||||
|
z-index: 2;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #d6111e;
|
||||||
|
border-color: #d6111e;
|
||||||
|
}
|
||||||
138
addons/dine360_dashboard/static/src/css/website_style.css
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
/* Custom Website Header Styling */
|
||||||
|
|
||||||
|
/* Force Header Background to Black */
|
||||||
|
header#top,
|
||||||
|
body .o_header_standard,
|
||||||
|
body header.o_header_standard,
|
||||||
|
body .navbar,
|
||||||
|
body #o_main_nav {
|
||||||
|
background-color: #111 !important;
|
||||||
|
/* background: #171422 !important; */
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navbar Links / Menu Items */
|
||||||
|
header#top .navbar-nav .nav-link,
|
||||||
|
header#top .nav-link,
|
||||||
|
body .o_header_standard .nav-link,
|
||||||
|
body .navbar .nav-link,
|
||||||
|
body #o_main_nav .nav-link {
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navbar Icons (Search, Cart, User, etc.) */
|
||||||
|
header#top i,
|
||||||
|
header#top .fa,
|
||||||
|
body .o_header_standard i,
|
||||||
|
body .o_header_standard .fa,
|
||||||
|
body .navbar i,
|
||||||
|
body .navbar .fa {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Brand / Logo Text (if text logo) */
|
||||||
|
header#top .navbar-brand,
|
||||||
|
body .o_header_standard .navbar-brand {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover States - Using Theme Teal */
|
||||||
|
header#top .nav-link:hover,
|
||||||
|
body .o_header_standard .nav-link:hover,
|
||||||
|
header#top i:hover,
|
||||||
|
body .o_header_standard i:hover {
|
||||||
|
color: #2BB1A5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active States - Using Theme Gold */
|
||||||
|
header#top .nav-link.active,
|
||||||
|
body .o_header_standard .nav-link.active {
|
||||||
|
color: #2BB1A5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown Menu overrides (ensure visibility) */
|
||||||
|
header#top .dropdown-menu,
|
||||||
|
body .o_header_standard .dropdown-menu {
|
||||||
|
background-color: #171422 !important;
|
||||||
|
border: 1px solid rgba(43, 177, 165, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
header#top .dropdown-item,
|
||||||
|
body .o_header_standard .dropdown-item {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
header#top .dropdown-item:hover,
|
||||||
|
body .o_header_standard .dropdown-item:hover {
|
||||||
|
background-color: #2BB1A5 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header Cart & Search Icon Adjustments */
|
||||||
|
.o_wsale_my_cart,
|
||||||
|
.o_nav_item_search,
|
||||||
|
.o_nav_item_cart,
|
||||||
|
header .navbar-nav .nav-item a[href="/shop/cart"],
|
||||||
|
header .navbar-nav .nav-item a.o_navlink_background_hover {
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover background for Search and Cart icons */
|
||||||
|
.o_wsale_my_cart:hover,
|
||||||
|
.o_nav_item_search:hover,
|
||||||
|
.o_nav_item_cart:hover,
|
||||||
|
header .navbar-nav .nav-item a[href="/shop/cart"]:hover,
|
||||||
|
header .o_search_button:hover {
|
||||||
|
background-color: #FECD4F !important;
|
||||||
|
border-radius: 50px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure icons turn dark on yellow background hover */
|
||||||
|
.o_wsale_my_cart:hover i,
|
||||||
|
.o_nav_item_search:hover i,
|
||||||
|
.o_nav_item_cart:hover i,
|
||||||
|
header .navbar-nav .nav-item a[href="/shop/cart"]:hover i,
|
||||||
|
header .o_search_button:hover i {
|
||||||
|
color: #111 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cart Count Badge Color */
|
||||||
|
.o_wsale_my_cart .badge,
|
||||||
|
.o_wsale_my_cart .o_wsale_cart_quantity,
|
||||||
|
header .navbar-nav .nav-item .badge-pill,
|
||||||
|
header .navbar-nav .nav-item .badge {
|
||||||
|
background-color: #2BB1A5 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header Action Buttons */
|
||||||
|
header .btn,
|
||||||
|
header .btn-primary,
|
||||||
|
header .o_header_cta_btn,
|
||||||
|
header .btn-contact,
|
||||||
|
header a.btn[href="/contactus"],
|
||||||
|
header a.btn[href*="contact"] {
|
||||||
|
background-color: #FECD4F !important;
|
||||||
|
color: #04121D !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
padding: 10px 25px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
text-transform: uppercase !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
box-shadow: 0 4px 10px rgba(254, 205, 79, 0.3) !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
header .btn:hover,
|
||||||
|
header .btn-primary:hover,
|
||||||
|
header .o_header_cta_btn:hover,
|
||||||
|
header .btn-contact:hover,
|
||||||
|
header a.btn[href="/contactus"]:hover,
|
||||||
|
header a.btn[href*="contact"]:hover {
|
||||||
|
background-color: #FECD4F !important;
|
||||||
|
color: #FECD4F !important;
|
||||||
|
transform: translateY(-2px) !important;
|
||||||
|
}
|
||||||
BIN
addons/dine360_dashboard/static/src/img/dashboard_bg.png
Normal file
|
After Width: | Height: | Size: 1014 KiB |
28
addons/dine360_dashboard/static/src/img/icons/apps.svg
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#d6111e;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#8b0d16;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="grad2" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#ff6b7a;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#d6111e;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="256" height="256" rx="56" fill="white"/>
|
||||||
|
<!-- 3x3 App grid -->
|
||||||
|
<rect x="44" y="44" width="56" height="56" rx="14" fill="url(#grad1)"/>
|
||||||
|
<rect x="114" y="44" width="56" height="56" rx="14" fill="url(#grad2)"/>
|
||||||
|
<rect x="156" y="44" width="56" height="56" rx="14" fill="#ffb347" opacity="0.85"/>
|
||||||
|
|
||||||
|
<rect x="44" y="114" width="56" height="56" rx="14" fill="#ffb347" opacity="0.85"/>
|
||||||
|
<rect x="114" y="114" width="56" height="56" rx="14" fill="url(#grad1)"/>
|
||||||
|
<rect x="184" y="114" width="56" height="56" rx="14" fill="url(#grad2)"/>
|
||||||
|
|
||||||
|
<rect x="44" y="184" width="56" height="28" rx="14" fill="url(#grad2)"/>
|
||||||
|
<rect x="114" y="184" width="56" height="28" rx="14" fill="#ffb347" opacity="0.85"/>
|
||||||
|
<rect x="184" y="184" width="28" height="28" rx="14" fill="url(#grad1)"/>
|
||||||
|
|
||||||
|
<!-- Plus icon in center of middle grid -->
|
||||||
|
<text x="142" y="149" font-family="Arial" font-size="32" font-weight="bold" fill="white" text-anchor="middle">+</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
32
addons/dine360_dashboard/static/src/img/icons/calendar.svg
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#ffffff;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#fff5f5;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#d6111e;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#8b0d16;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="256" height="256" rx="56" fill="url(#bg)"/>
|
||||||
|
<!-- Calendar body -->
|
||||||
|
<rect x="44" y="76" width="168" height="144" rx="16" fill="white" stroke="#e8e8e8" stroke-width="3"/>
|
||||||
|
<!-- Header -->
|
||||||
|
<rect x="44" y="76" width="168" height="52" rx="16" fill="url(#grad1)"/>
|
||||||
|
<rect x="44" y="104" width="168" height="24" fill="#d6111e"/>
|
||||||
|
<!-- Calendar pins -->
|
||||||
|
<rect x="86" y="58" width="14" height="36" rx="7" fill="#555"/>
|
||||||
|
<rect x="156" y="58" width="14" height="36" rx="7" fill="#555"/>
|
||||||
|
<!-- Day number -->
|
||||||
|
<text x="128" y="116" font-family="Arial" font-size="28" font-weight="bold" fill="white" text-anchor="middle">31</text>
|
||||||
|
<!-- Grid dots -->
|
||||||
|
<circle cx="80" cy="158" r="7" fill="#d6111e"/>
|
||||||
|
<circle cx="112" cy="158" r="7" fill="#ccc"/>
|
||||||
|
<circle cx="144" cy="158" r="7" fill="#ccc"/>
|
||||||
|
<circle cx="176" cy="158" r="7" fill="#ccc"/>
|
||||||
|
<circle cx="80" cy="186" r="7" fill="#ccc"/>
|
||||||
|
<circle cx="112" cy="186" r="7" fill="#d6111e"/>
|
||||||
|
<circle cx="144" cy="186" r="7" fill="#ccc"/>
|
||||||
|
<circle cx="176" cy="186" r="7" fill="#ccc"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
17
addons/dine360_dashboard/static/src/img/icons/contacts.svg
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#d6111e;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#8b0d16;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="256" height="256" rx="56" fill="white"/>
|
||||||
|
<!-- Person head -->
|
||||||
|
<circle cx="128" cy="96" r="40" fill="url(#grad1)"/>
|
||||||
|
<!-- Person body -->
|
||||||
|
<path d="M60 210 Q60 158 128 158 Q196 158 196 210 Z" fill="url(#grad1)"/>
|
||||||
|
<!-- Book lines -->
|
||||||
|
<rect x="36" y="100" width="16" height="8" rx="3" fill="#d6111e" opacity="0.5"/>
|
||||||
|
<rect x="36" y="120" width="16" height="8" rx="3" fill="#d6111e" opacity="0.5"/>
|
||||||
|
<rect x="36" y="140" width="16" height="8" rx="3" fill="#d6111e" opacity="0.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 823 B |
17
addons/dine360_dashboard/static/src/img/icons/crm.svg
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#d6111e;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#8b0d16;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="grad2" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#ff6b7a;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#d6111e;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="256" height="256" rx="56" fill="white"/>
|
||||||
|
<!-- Funnel/Pipeline -->
|
||||||
|
<polygon points="48,60 208,60 168,120 88,120" fill="url(#grad1)"/>
|
||||||
|
<polygon points="88,128 168,128 148,178 108,178" fill="url(#grad2)"/>
|
||||||
|
<polygon points="108,185 148,185 135,220 121,220" fill="#8b0d16"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 842 B |
21
addons/dine360_dashboard/static/src/img/icons/dashboards.svg
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#d6111e;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#8b0d16;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="256" height="256" rx="56" fill="white"/>
|
||||||
|
<!-- Dashboard grid panels -->
|
||||||
|
<rect x="40" y="40" width="78" height="78" rx="14" fill="url(#grad1)"/>
|
||||||
|
<rect x="138" y="40" width="78" height="78" rx="14" fill="#ff6b7a" opacity="0.8"/>
|
||||||
|
<rect x="40" y="138" width="78" height="78" rx="14" fill="#ff6b7a" opacity="0.6"/>
|
||||||
|
<rect x="138" y="138" width="78" height="78" rx="14" fill="url(#grad1)"/>
|
||||||
|
<!-- Bar chart inside top-left -->
|
||||||
|
<rect x="54" y="82" width="10" height="22" rx="3" fill="white"/>
|
||||||
|
<rect x="70" y="70" width="10" height="34" rx="3" fill="white" opacity="0.8"/>
|
||||||
|
<rect x="86" y="75" width="10" height="29" rx="3" fill="white" opacity="0.6"/>
|
||||||
|
<!-- Pie slice inside bottom-right -->
|
||||||
|
<path d="M177 177 L177 157 A20 20 0 0 1 197 177 Z" fill="white"/>
|
||||||
|
<path d="M177 177 L197 177 A20 20 0 0 1 167 195 Z" fill="white" opacity="0.7"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
22
addons/dine360_dashboard/static/src/img/icons/discuss.svg
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#ffffff;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#fff5f5;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#d6111e;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#8b0d16;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="256" height="256" rx="56" fill="url(#bg)"/>
|
||||||
|
<!-- Main chat bubble -->
|
||||||
|
<rect x="50" y="70" width="140" height="100" rx="20" fill="url(#grad1)"/>
|
||||||
|
<polygon points="70,170 70,200 100,170" fill="#d6111e"/>
|
||||||
|
<!-- Dots inside bubble -->
|
||||||
|
<circle cx="98" cy="120" r="10" fill="white"/>
|
||||||
|
<circle cx="128" cy="120" r="10" fill="white" opacity="0.8"/>
|
||||||
|
<circle cx="158" cy="120" r="10" fill="white" opacity="0.6"/>
|
||||||
|
<!-- Small secondary bubble -->
|
||||||
|
<rect x="140" y="155" width="76" height="52" rx="14" fill="#ff6b7a" opacity="0.3"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
19
addons/dine360_dashboard/static/src/img/icons/employees.svg
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#d6111e;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#8b0d16;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="256" height="256" rx="56" fill="white"/>
|
||||||
|
<!-- Left person head -->
|
||||||
|
<circle cx="88" cy="92" r="30" fill="url(#grad1)" opacity="0.75"/>
|
||||||
|
<!-- Right person head -->
|
||||||
|
<circle cx="168" cy="92" r="30" fill="url(#grad1)"/>
|
||||||
|
<!-- Left person body -->
|
||||||
|
<path d="M34 210 Q34 160 88 160 Q120 160 126 175 Q114 158 88 158 Q38 158 38 210 Z" fill="#d6111e" opacity="0.6"/>
|
||||||
|
<path d="M34 215 Q34 160 88 160 Q118 160 126 178" fill="none" stroke="#d6111e" stroke-width="0"/>
|
||||||
|
<path d="M28 215 Q30 155 88 155 Q122 155 130 180" fill="#d6111e" opacity="0.55"/>
|
||||||
|
<!-- Right/main person body -->
|
||||||
|
<path d="M100 215 Q100 158 168 158 Q236 158 236 215 Z" fill="url(#grad1)"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 996 B |
23
addons/dine360_dashboard/static/src/img/icons/inventory.svg
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#d6111e;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#8b0d16;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="grad2" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#ff6b7a;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#d6111e;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="256" height="256" rx="56" fill="white"/>
|
||||||
|
<!-- Bottom large box -->
|
||||||
|
<rect x="40" y="162" width="176" height="62" rx="12" fill="url(#grad1)"/>
|
||||||
|
<!-- Middle box -->
|
||||||
|
<rect x="60" y="108" width="136" height="62" rx="12" fill="url(#grad2)"/>
|
||||||
|
<!-- Top small box -->
|
||||||
|
<rect x="84" y="60" width="88" height="54" rx="12" fill="url(#grad1)" opacity="0.8"/>
|
||||||
|
<!-- Box lines/lids -->
|
||||||
|
<line x1="40" y1="180" x2="216" y2="180" stroke="white" stroke-width="3" opacity="0.4"/>
|
||||||
|
<line x1="60" y1="126" x2="196" y2="126" stroke="white" stroke-width="3" opacity="0.4"/>
|
||||||
|
<line x1="84" y1="78" x2="172" y2="78" stroke="white" stroke-width="3" opacity="0.4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
25
addons/dine360_dashboard/static/src/img/icons/invoicing.svg
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#d6111e;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#8b0d16;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="256" height="256" rx="56" fill="white"/>
|
||||||
|
<!-- Invoice paper -->
|
||||||
|
<rect x="56" y="44" width="144" height="180" rx="14" fill="white" stroke="#eee" stroke-width="3"/>
|
||||||
|
<!-- Invoice fold corner -->
|
||||||
|
<polygon points="168,44 200,76 168,76" fill="url(#grad1)"/>
|
||||||
|
<!-- Header bar -->
|
||||||
|
<rect x="56" y="44" width="112" height="32" rx="14" fill="url(#grad1)"/>
|
||||||
|
<rect x="56" y="62" width="112" height="14" fill="#d6111e"/>
|
||||||
|
<!-- Invoice lines -->
|
||||||
|
<rect x="76" y="96" width="104" height="8" rx="4" fill="#eee"/>
|
||||||
|
<rect x="76" y="114" width="80" height="8" rx="4" fill="#eee"/>
|
||||||
|
<rect x="76" y="132" width="92" height="8" rx="4" fill="#eee"/>
|
||||||
|
<!-- Dollar amount highlight -->
|
||||||
|
<rect x="76" y="158" width="104" height="24" rx="6" fill="#fff0f0"/>
|
||||||
|
<text x="128" y="175" font-family="Arial" font-size="16" font-weight="bold" fill="#d6111e" text-anchor="middle">$ 128.00</text>
|
||||||
|
<!-- Footer tag -->
|
||||||
|
<rect x="76" y="196" width="104" height="12" rx="4" fill="url(#grad1)" opacity="0.3"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@ -0,0 +1,23 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#ffffff;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#fff5f5;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#d6111e;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#8b0d16;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="256" height="256" rx="56" fill="url(#bg)"/>
|
||||||
|
<!-- Chef hat brim -->
|
||||||
|
<rect x="58" y="158" width="140" height="28" rx="8" fill="url(#grad1)"/>
|
||||||
|
<!-- Chef hat body -->
|
||||||
|
<ellipse cx="128" cy="118" rx="62" ry="56" fill="white" stroke="#e0e0e0" stroke-width="3"/>
|
||||||
|
<!-- Chef hat top bump -->
|
||||||
|
<ellipse cx="128" cy="82" rx="30" ry="28" fill="white" stroke="#e0e0e0" stroke-width="3"/>
|
||||||
|
<!-- D360 text on hat -->
|
||||||
|
<text x="128" y="145" font-family="Arial" font-size="18" font-weight="bold" fill="#d6111e" text-anchor="middle">D360</text>
|
||||||
|
<!-- Fork and spoon cross below -->
|
||||||
|
<line x1="100" y1="195" x2="156" y2="195" stroke="white" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,26 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#d6111e;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#8b0d16;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="256" height="256" rx="56" fill="white"/>
|
||||||
|
<!-- POS terminal screen -->
|
||||||
|
<rect x="62" y="50" width="132" height="100" rx="14" fill="url(#grad1)"/>
|
||||||
|
<!-- Screen glare -->
|
||||||
|
<rect x="74" y="62" width="108" height="76" rx="8" fill="white" opacity="0.15"/>
|
||||||
|
<!-- Screen content lines -->
|
||||||
|
<rect x="82" y="74" width="60" height="10" rx="4" fill="white" opacity="0.8"/>
|
||||||
|
<rect x="82" y="92" width="40" height="8" rx="4" fill="white" opacity="0.6"/>
|
||||||
|
<rect x="150" y="88" width="24" height="18" rx="4" fill="white"/>
|
||||||
|
<!-- POS stand/neck -->
|
||||||
|
<rect x="114" y="150" width="28" height="18" rx="4" fill="#8b0d16"/>
|
||||||
|
<!-- POS base -->
|
||||||
|
<ellipse cx="128" cy="178" rx="52" ry="14" fill="url(#grad1)"/>
|
||||||
|
<!-- Keypad dots -->
|
||||||
|
<circle cx="88" cy="200" r="6" fill="#d6111e"/>
|
||||||
|
<circle cx="110" cy="200" r="6" fill="#d6111e"/>
|
||||||
|
<circle cx="132" cy="200" r="6" fill="#d6111e"/>
|
||||||
|
<circle cx="154" cy="200" r="6" fill="#d6111e"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
18
addons/dine360_dashboard/static/src/img/icons/purchase.svg
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#d6111e;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#8b0d16;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="256" height="256" rx="56" fill="white"/>
|
||||||
|
<!-- Shopping bag body -->
|
||||||
|
<path d="M60 110 L70 210 Q70 220 80 220 L176 220 Q186 220 186 210 L196 110 Z" fill="url(#grad1)"/>
|
||||||
|
<!-- Bag handle left -->
|
||||||
|
<path d="M96 110 Q96 62 128 62 Q160 62 160 110" fill="none" stroke="#8b0d16" stroke-width="14" stroke-linecap="round"/>
|
||||||
|
<!-- Bag top fold highlight -->
|
||||||
|
<rect x="60" y="100" width="136" height="18" rx="6" fill="#8b0d16" opacity="0.5"/>
|
||||||
|
<!-- PO badge -->
|
||||||
|
<rect x="86" y="148" width="84" height="36" rx="10" fill="white" opacity="0.25"/>
|
||||||
|
<text x="128" y="172" font-family="Arial" font-size="18" font-weight="bold" fill="white" text-anchor="middle">PO</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 999 B |
18
addons/dine360_dashboard/static/src/img/icons/sales.svg
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#d6111e;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#8b0d16;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="256" height="256" rx="56" fill="white"/>
|
||||||
|
<!-- Trending arrow up -->
|
||||||
|
<polyline points="44,180 96,124 136,152 212,72" fill="none" stroke="url(#grad1)" stroke-width="18" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<!-- Arrow head -->
|
||||||
|
<polygon points="212,72 212,110 180,82" fill="#d6111e"/>
|
||||||
|
<!-- Dots on line -->
|
||||||
|
<circle cx="44" cy="180" r="10" fill="#d6111e"/>
|
||||||
|
<circle cx="96" cy="124" r="10" fill="#d6111e"/>
|
||||||
|
<circle cx="136" cy="152" r="10" fill="#d6111e"/>
|
||||||
|
<circle cx="212" cy="72" r="10" fill="#d6111e"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 864 B |
25
addons/dine360_dashboard/static/src/img/icons/settings.svg
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#d6111e;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#8b0d16;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="256" height="256" rx="56" fill="white"/>
|
||||||
|
<!-- Outer gear ring -->
|
||||||
|
<circle cx="128" cy="128" r="76" fill="none" stroke="url(#grad1)" stroke-width="20"/>
|
||||||
|
<!-- Gear teeth (8 teeth) -->
|
||||||
|
<rect x="116" y="36" width="24" height="30" rx="8" fill="url(#grad1)"/>
|
||||||
|
<rect x="116" y="190" width="24" height="30" rx="8" fill="url(#grad1)"/>
|
||||||
|
<rect x="36" y="116" width="30" height="24" rx="8" fill="url(#grad1)"/>
|
||||||
|
<rect x="190" y="116" width="30" height="24" rx="8" fill="url(#grad1)"/>
|
||||||
|
<!-- Diagonal teeth -->
|
||||||
|
<rect x="60" y="60" width="24" height="30" rx="8" fill="url(#grad1)" transform="rotate(45 72 75)"/>
|
||||||
|
<rect x="172" y="60" width="24" height="30" rx="8" fill="url(#grad1)" transform="rotate(-45 184 75)"/>
|
||||||
|
<rect x="60" y="166" width="24" height="30" rx="8" fill="url(#grad1)" transform="rotate(-45 72 181)"/>
|
||||||
|
<rect x="172" y="166" width="24" height="30" rx="8" fill="url(#grad1)" transform="rotate(45 184 181)"/>
|
||||||
|
<!-- Inner circle -->
|
||||||
|
<circle cx="128" cy="128" r="42" fill="url(#grad1)"/>
|
||||||
|
<!-- Center dot -->
|
||||||
|
<circle cx="128" cy="128" r="18" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
@ -0,0 +1,21 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#d6111e;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#8b0d16;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="256" height="256" rx="56" fill="white"/>
|
||||||
|
<!-- Table top -->
|
||||||
|
<ellipse cx="128" cy="118" rx="88" ry="22" fill="url(#grad1)"/>
|
||||||
|
<!-- Table leg -->
|
||||||
|
<rect x="118" y="138" width="20" height="62" rx="6" fill="#8b0d16"/>
|
||||||
|
<!-- Table base -->
|
||||||
|
<ellipse cx="128" cy="200" rx="50" ry="12" fill="#d6111e" opacity="0.5"/>
|
||||||
|
<!-- Reserved tag -->
|
||||||
|
<rect x="90" y="75" width="76" height="36" rx="8" fill="white" stroke="#d6111e" stroke-width="3"/>
|
||||||
|
<text x="128" y="99" font-family="Arial" font-size="14" font-weight="bold" fill="#d6111e" text-anchor="middle">RESERVED</text>
|
||||||
|
<!-- Chairs -->
|
||||||
|
<ellipse cx="54" cy="118" rx="20" ry="10" fill="#ff6b7a" opacity="0.7"/>
|
||||||
|
<ellipse cx="202" cy="118" rx="20" ry="10" fill="#ff6b7a" opacity="0.7"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@ -0,0 +1,25 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#d6111e;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#8b0d16;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="256" height="256" rx="56" fill="white"/>
|
||||||
|
<!-- Car body -->
|
||||||
|
<rect x="30" y="140" width="196" height="56" rx="20" fill="url(#grad1)"/>
|
||||||
|
<!-- Car roof -->
|
||||||
|
<path d="M72 140 Q90 96 140 96 L186 96 Q210 96 220 140 Z" fill="url(#grad1)"/>
|
||||||
|
<!-- Windows -->
|
||||||
|
<path d="M90 140 Q100 108 128 108 L160 108 Q178 108 186 140 Z" fill="white" opacity="0.3"/>
|
||||||
|
<!-- Delivery bag on top -->
|
||||||
|
<rect x="100" y="68" width="56" height="42" rx="10" fill="#ff6b7a"/>
|
||||||
|
<rect x="112" y="58" width="32" height="18" rx="8" fill="#ff6b7a" stroke="white" stroke-width="3" fill-opacity="0"/>
|
||||||
|
<!-- U letter (Uber) -->
|
||||||
|
<text x="128" y="98" font-family="Arial" font-size="22" font-weight="bold" fill="white" text-anchor="middle">U</text>
|
||||||
|
<!-- Wheels -->
|
||||||
|
<circle cx="80" cy="196" r="22" fill="#8b0d16"/>
|
||||||
|
<circle cx="80" cy="196" r="12" fill="white"/>
|
||||||
|
<circle cx="176" cy="196" r="22" fill="#8b0d16"/>
|
||||||
|
<circle cx="176" cy="196" r="12" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
25
addons/dine360_dashboard/static/src/img/icons/website.svg
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#d6111e;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#8b0d16;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="256" height="256" rx="56" fill="white"/>
|
||||||
|
<!-- Globe/earth circle -->
|
||||||
|
<circle cx="128" cy="128" r="90" fill="none" stroke="#eee" stroke-width="3"/>
|
||||||
|
<circle cx="128" cy="128" r="90" fill="none" stroke="url(#grad1)" stroke-width="14" stroke-dasharray="60 510" stroke-dashoffset="-10"/>
|
||||||
|
<!-- Latitude lines -->
|
||||||
|
<ellipse cx="128" cy="128" rx="90" ry="38" fill="none" stroke="#d6111e" stroke-width="3" opacity="0.4"/>
|
||||||
|
<!-- Meridian (vertical oval) -->
|
||||||
|
<ellipse cx="128" cy="128" rx="44" ry="90" fill="none" stroke="#d6111e" stroke-width="3" opacity="0.4"/>
|
||||||
|
<!-- Center vertical line -->
|
||||||
|
<line x1="128" y1="38" x2="128" y2="218" stroke="#d6111e" stroke-width="3" opacity="0.4"/>
|
||||||
|
<!-- Horizontal center -->
|
||||||
|
<line x1="38" y1="128" x2="218" y2="128" stroke="#d6111e" stroke-width="3" opacity="0.4"/>
|
||||||
|
<!-- Outer circle -->
|
||||||
|
<circle cx="128" cy="128" r="90" fill="none" stroke="url(#grad1)" stroke-width="6"/>
|
||||||
|
<!-- Dine360 center badge -->
|
||||||
|
<circle cx="128" cy="128" r="28" fill="url(#grad1)"/>
|
||||||
|
<text x="128" y="134" font-family="Arial" font-size="13" font-weight="bold" fill="white" text-anchor="middle">D360</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
BIN
addons/dine360_dashboard/static/src/img/login_bg.png
Normal file
|
After Width: | Height: | Size: 760 KiB |
31
addons/dine360_dashboard/static/src/js/chennora_title.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { WebClient } from "@web/webclient/webclient";
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
import { onMounted } from "@odoo/owl";
|
||||||
|
|
||||||
|
patch(WebClient.prototype, {
|
||||||
|
setup() {
|
||||||
|
super.setup();
|
||||||
|
try {
|
||||||
|
this.title.setParts({ brand: "Shivasakthi" });
|
||||||
|
} catch (e) {
|
||||||
|
// title service not ready yet, ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
try {
|
||||||
|
// Only redirect to home dashboard if the URL has no meaningful hash
|
||||||
|
// (i.e., user just typed /web or /web# directly)
|
||||||
|
const hash = window.location.hash;
|
||||||
|
const path = window.location.pathname;
|
||||||
|
if (path === "/web" && (!hash || hash === "#" || hash === "#home")) {
|
||||||
|
window.location.href = "/";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore redirect errors
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
14
addons/dine360_dashboard/static/src/xml/navbar_extension.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-inherit="web.NavBar" t-inherit-mode="extension" owl="1">
|
||||||
|
<xpath expr="//t[@t-call='web.NavBar.AppsMenu']" position="after">
|
||||||
|
<a t-if="!env.services.user.isPublic" href="/" class="o_navbar_dashboard_btn d-none d-md-flex align-items-center px-4"
|
||||||
|
style="background-color: #d61112; color: white !important; font-weight: bold; border-left: 1px solid rgba(255,255,255,0.2); text-decoration: none; height: 100%; -webkit-app-region: no-drag;"
|
||||||
|
title="Back to Main Dashboard">
|
||||||
|
<i class="fa fa-th-large me-2"></i>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
102
addons/dine360_dashboard/views/home_template.xml
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<odoo>
|
||||||
|
<template id="image_home_template" name="Home Page Icons">
|
||||||
|
<t t-call="web.frontend_layout">
|
||||||
|
<t t-set="no_footer" t-value="True"/>
|
||||||
|
<t t-set="no_header" t-value="True"/>
|
||||||
|
<t t-set="pageName" t-value="'home_dashboard'"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/dine360_dashboard/static/src/css/home_menu.css?v=1.1"/>
|
||||||
|
|
||||||
|
<div class="o_home_menu_background">
|
||||||
|
<!-- Top Bar -->
|
||||||
|
<div class="o_home_top_bar">
|
||||||
|
<div class="o_top_bar_island">
|
||||||
|
<a href="/" class="o_top_item" title="Shivasakthi Home" style="width: auto; padding: 0 10px;">
|
||||||
|
<img t-attf-src="/web/binary/company_logo" style="height: 24px; width: auto; object-fit: contain;"/>
|
||||||
|
</a>
|
||||||
|
<div class="o_bar_divider" style="margin: 0 5px;"></div>
|
||||||
|
|
||||||
|
<a href="/" class="o_top_item" title="View Website">
|
||||||
|
<i class="fa fa-globe"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/web#action=mail.action_discuss" class="o_top_item" title="Messages">
|
||||||
|
<i class="fa fa-comments-o"/>
|
||||||
|
<span class="badge_dot"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Only show Settings for Admins -->
|
||||||
|
<t t-set="is_admin" t-value="user_id.has_group('base.group_system') or user_id.has_group('dine360_restaurant.group_restaurant_admin')"/>
|
||||||
|
<t t-if="is_admin">
|
||||||
|
<a href="/web#action=base.action_res_config_general_settings" class="o_top_item" title="Settings">
|
||||||
|
<i class="fa fa-sliders"/>
|
||||||
|
</a>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<div class="o_bar_divider"></div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="o_user_avatar_container">
|
||||||
|
<a href="/web#action=base.action_res_users_my" class="o_user_avatar" style="display:flex; align-items:center; justify-content:center; text-decoration:none;">
|
||||||
|
<t t-esc="user_id.name[0] if user_id else 'U'"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container" style="padding-top: 100px;">
|
||||||
|
<!-- Low Stock Alert Section -->
|
||||||
|
<t t-if="low_stock_products">
|
||||||
|
<div class="o_low_stock_alert mb-5" style="background: rgba(220, 53, 69, 0.1); border: 1px solid rgba(220, 53, 69, 0.2); border-radius: 12px; padding: 15px 25px;">
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="fa fa-exclamation-triangle me-2" style="color: #dc3545; font-size: 1.2rem;"></i>
|
||||||
|
<h5 class="mb-0 fw-bold" style="color: #dc3545;">Low Stock Alert!</h5>
|
||||||
|
<a href="/web#action=stock.product_template_action_product" class="ms-auto text-decoration-none small fw-bold" style="color: #dc3545;">
|
||||||
|
Manage Inventory <i class="fa fa-arrow-right ms-1"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<t t-foreach="low_stock_products" t-as="product">
|
||||||
|
<div class="badge rounded-pill bg-white border px-3 py-2 shadow-sm" style="color: #333;">
|
||||||
|
<span class="fw-bold" t-esc="product.name"/>:
|
||||||
|
<span class="text-danger fw-bold" t-esc="product.qty_available"/>
|
||||||
|
<span class="text-muted" t-esc="product.uom_id.name"/>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<div class="o_apps">
|
||||||
|
|
||||||
|
<t t-foreach="menus" t-as="menu">
|
||||||
|
<t t-set="app_url" t-value="'/web#menu_id=' + str(menu.id)"/>
|
||||||
|
<!-- Check if it is the Website app by checking the icon module or name -->
|
||||||
|
<t t-if="(menu.web_icon and menu.web_icon.startswith('website,')) or menu.name == 'Website'">
|
||||||
|
<t t-set="app_url" t-value="'/home'"/>
|
||||||
|
</t>
|
||||||
|
<a t-att-href="app_url" class="o_app">
|
||||||
|
<div class="o_app_icon_container">
|
||||||
|
<t t-if="menu.web_icon">
|
||||||
|
<t t-set="icon_data" t-value="menu.web_icon.split(',')"/>
|
||||||
|
<img t-attf-src="/{{icon_data[0]}}/{{icon_data[1]}}"
|
||||||
|
class="o_app_icon" loading="lazy"
|
||||||
|
onerror="this.style.display='none'; this.nextElementSibling.style.display='block';"/>
|
||||||
|
<div class="o_app_icon fa fa-cube" style="font-size: 40px; color: #7c7bad; display: none;"/>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<div class="o_app_icon fa fa-cube" style="font-size: 40px; color: #7c7bad;"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
<div class="o_app_name">
|
||||||
|
<t t-esc="menu.name"/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
</odoo>
|
||||||
|
|
||||||
45
addons/dine360_dashboard/views/login_templates.xml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<odoo>
|
||||||
|
<template id="custom_login_layout" inherit_id="web.login_layout" name="Custom Login Layout" priority="10">
|
||||||
|
<xpath expr="//t[@t-call='web.frontend_layout']" position="inside">
|
||||||
|
<t t-set="body_classname" t-value="'o_custom_login_body'"/>
|
||||||
|
<t t-set="no_header" t-value="True"/>
|
||||||
|
<t t-set="no_footer" t-value="True"/>
|
||||||
|
<t t-set="head">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/dine360_dashboard/static/src/css/login_style.css"/>
|
||||||
|
</t>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
<xpath expr="//div[contains(@class,'container')]" position="replace">
|
||||||
|
<div class="o_login_main_wrapper">
|
||||||
|
<div class="o_login_left_side">
|
||||||
|
<!-- Space for right alignment -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_login_right_side">
|
||||||
|
<div class="o_login_card">
|
||||||
|
<div class="o_login_logo_container">
|
||||||
|
<img t-attf-src="/web/binary/company_logo{{ '?db=' + request.db if request.db else '' }}" alt="Logo"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_login_header_text text-center">
|
||||||
|
<h3>Login</h3>
|
||||||
|
<p style="color: rgba(255, 255, 255, 0.6) !important;">Welcome back! Please enter your details.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_login_form_container">
|
||||||
|
<t t-out="0"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_login_footer text-center mt-4">
|
||||||
|
<a href="/web/database/selector" t-if="not disable_database_manager" class="small text-decoration-none" style="color: rgba(255, 255, 255, 0.5) !important;">Manage Databases</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_login_footer_custom">
|
||||||
|
<p>Powered by <a href="https://dine360.com/" target="_blank">Dine360 Inc</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
</odoo>
|
||||||
17
addons/dine360_dashboard/views/shop_template.xml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<odoo>
|
||||||
|
<!-- Ensure Shop Filter Sidebar is Visible -->
|
||||||
|
<template id="products_categories_inherit" inherit_id="website_sale.products_categories" name="Show Categories Sidebar">
|
||||||
|
<!-- Force display of categories sidebar -->
|
||||||
|
<!-- <xpath expr="//div[@id='products_grid_before']" position="attributes">
|
||||||
|
<attribute name="class">col-lg-3 d-block</attribute>
|
||||||
|
<attribute name="style">display: block !important; visibility: visible !important;</attribute>
|
||||||
|
</xpath> -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Ensure Products Grid Takes Remaining Space -->
|
||||||
|
<!-- <template id="products_grid_inherit" inherit_id="website_sale.products" name="Adjust Products Grid">
|
||||||
|
<xpath expr="//div[@id='products_grid']" position="attributes">
|
||||||
|
<attribute name="class">col-lg-9</attribute>
|
||||||
|
</xpath>
|
||||||
|
</template> -->
|
||||||
|
</odoo>
|
||||||
25
addons/dine360_dashboard/views/web_title_template.xml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<odoo>
|
||||||
|
<template id="shivasakthi_web_layout" inherit_id="web.layout" name="Shivasakthi Title">
|
||||||
|
<xpath expr="//title" position="replace">
|
||||||
|
<title>Shivasakthi</title>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//link[@rel='shortcut icon']" position="replace">
|
||||||
|
<link rel="shortcut icon" href="/dine360_theme_shivasakthi/static/description/icon.png" type="image/x-icon"/>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
<xpath expr="//body" position="inside">
|
||||||
|
<t t-set="is_editor" t-value="request.params.get('enable_editor') or request.params.get('edit')"/>
|
||||||
|
<t t-if="not request.env.user._is_public() and not is_editor">
|
||||||
|
<a href="/" class="o_dashboard_return_btn d-print-none" title="Back to Dashboard"
|
||||||
|
style="position: fixed; bottom: 20px; right: 20px; z-index: 9999;
|
||||||
|
background-color: #d6111e; color: #ffff !important; padding: 12px 24px;
|
||||||
|
border-radius: 50px; text-decoration: none; font-weight: bold;
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.2); display: flex; align-items: center;
|
||||||
|
gap: 8px; transition: all 0.3s ease; border: 2px solid #ffffffff !important; font-family: sans-serif;">
|
||||||
|
<i class="fa fa-th-large" style="font-size: 18px;"></i>
|
||||||
|
<span>Back to Dashboard</span>
|
||||||
|
</a>
|
||||||
|
</t>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
</odoo>
|
||||||
14
addons/dine360_dashboard/views/website_logo.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<!--
|
||||||
|
Override the Shared Brand Placeholder
|
||||||
|
Using a generic selector '//span' because strict field matching failed.
|
||||||
|
This template typically contains a single span or img for the logo.
|
||||||
|
-->
|
||||||
|
<template id="custom_website_logo_placeholder" inherit_id="website.placeholder_header_brand" name="Custom Website Logo Placeholder">
|
||||||
|
<xpath expr="/*" position="replace">
|
||||||
|
<img t-att-src="'/web/image/res.company/%s/logo' % res_company.id" t-att-alt="website.name" class="img-fluid" style="max-height: 60px; width: auto; object-fit: contain;"/>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
1
addons/dine360_kds/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
38
addons/dine360_kds/__manifest__.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
'name': 'Dine360 Kitchen Display System',
|
||||||
|
'version': '1.0',
|
||||||
|
'category': 'Sales/Point of Sale',
|
||||||
|
'summary': 'Dedicated KDS for Restaurant Kitchen',
|
||||||
|
'description': """
|
||||||
|
Professional Kitchen Display System:
|
||||||
|
- Real-time order tracking
|
||||||
|
- Preparation status management
|
||||||
|
- Cooking time tracking
|
||||||
|
- Kanban dashboard for chefs
|
||||||
|
- Floor/Table based organization
|
||||||
|
""",
|
||||||
|
'author': 'Dine360',
|
||||||
|
'depends': ['dine360_restaurant', 'point_of_sale', 'pos_restaurant', 'sale_management', 'website_sale', 'dine360_order_channels'],
|
||||||
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'views/pos_order_line_views.xml',
|
||||||
|
'views/product_views.xml',
|
||||||
|
'views/kds_menus.xml',
|
||||||
|
],
|
||||||
|
'assets': {
|
||||||
|
'web.assets_backend': [
|
||||||
|
'dine360_kds/static/src/css/kds_style.css',
|
||||||
|
'dine360_kds/static/src/js/kds_backend.js',
|
||||||
|
],
|
||||||
|
'point_of_sale._assets_pos': [
|
||||||
|
'dine360_kds/static/src/css/pos_kds.css',
|
||||||
|
'dine360_kds/static/src/js/pos_kds.js',
|
||||||
|
# 'dine360_kds/static/src/xml/pos_kds.xml', # Temporarily disabled
|
||||||
|
],
|
||||||
|
|
||||||
|
},
|
||||||
|
'installable': True,
|
||||||
|
'application': True,
|
||||||
|
'icon': '/dine360_kds/static/description/icon.png',
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
4
addons/dine360_kds/models/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from . import pos_order_line
|
||||||
|
from . import product
|
||||||
|
from . import pos_session
|
||||||
|
from . import website_sale_integration
|
||||||
180
addons/dine360_kds/models/pos_order_line.py
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
from odoo import models, fields, api, _
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class PosOrderLine(models.Model):
|
||||||
|
_inherit = 'pos.order.line'
|
||||||
|
|
||||||
|
preparation_status = fields.Selection([
|
||||||
|
('waiting', 'Waiting'),
|
||||||
|
('preparing', 'Preparing'),
|
||||||
|
('ready', 'Ready'),
|
||||||
|
('served', 'Served'),
|
||||||
|
('cancelled', 'Cancelled')
|
||||||
|
], string='Preparation Status', default='waiting', tracking=True, group_expand='_read_group_preparation_status')
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _read_group_preparation_status(self, stages, domain, order):
|
||||||
|
return ['waiting', 'preparing', 'ready', 'served']
|
||||||
|
|
||||||
|
color = fields.Integer(string='Color', default=0)
|
||||||
|
preparation_time_start = fields.Datetime(string='Start Time')
|
||||||
|
preparation_time_end = fields.Datetime(string='Ready Time')
|
||||||
|
cooking_time = fields.Integer(string='Cooking Time (min)', compute='_compute_cooking_time', store=True)
|
||||||
|
|
||||||
|
table_id = fields.Many2one('restaurant.table', related='order_id.table_id', string='Table', store=True)
|
||||||
|
floor_id = fields.Many2one('restaurant.floor', related='order_id.table_id.floor_id', string='Floor', store=True)
|
||||||
|
order_source = fields.Selection([
|
||||||
|
('walk_in', 'Walk-In (Standard POS)'),
|
||||||
|
('phone', 'Telephone Order'),
|
||||||
|
('online', 'Online / eCommerce'),
|
||||||
|
('whatsapp', 'WhatsApp'),
|
||||||
|
('social_media', 'Social Media'),
|
||||||
|
('platform', 'Third-Party Platform'),
|
||||||
|
('kiosk', 'Self-Order Kiosk'),
|
||||||
|
('qr', 'QR Table Order'),
|
||||||
|
], related='order_id.order_source', string='Order Source', store=True)
|
||||||
|
fulfilment_type = fields.Selection([
|
||||||
|
('dine_in', 'Dine-In'),
|
||||||
|
('pickup', 'Pickup'),
|
||||||
|
('delivery', 'Delivery'),
|
||||||
|
], related='order_id.fulfilment_type', string='Fulfilment Type', store=True)
|
||||||
|
|
||||||
|
@api.depends('preparation_time_start', 'preparation_time_end')
|
||||||
|
def _compute_cooking_time(self):
|
||||||
|
for line in self:
|
||||||
|
if line.preparation_time_start and line.preparation_time_end:
|
||||||
|
diff = line.preparation_time_end - line.preparation_time_start
|
||||||
|
line.cooking_time = int(diff.total_seconds() / 60)
|
||||||
|
else:
|
||||||
|
line.cooking_time = 0
|
||||||
|
|
||||||
|
def _notify_pos(self):
|
||||||
|
"""Send notification to POS when order line status changes"""
|
||||||
|
_logger.info("=== _notify_pos called for %s lines ===" % len(self))
|
||||||
|
for line in self:
|
||||||
|
_logger.info(f"Processing line {line.id}, order: {line.order_id.name}, config: {line.order_id.config_id}")
|
||||||
|
if line.order_id.config_id:
|
||||||
|
channel_name = "pos_config_Channel_%s" % line.order_id.config_id.id
|
||||||
|
payload = {
|
||||||
|
'line_id': line.id,
|
||||||
|
'order_id': line.order_id.id,
|
||||||
|
'status': line.preparation_status,
|
||||||
|
'status_label': dict(self._fields['preparation_status']._description_selection(self.env)).get(line.preparation_status),
|
||||||
|
'order_uid': line.order_id.pos_reference,
|
||||||
|
'product_id': line.product_id.id,
|
||||||
|
'qty': line.qty,
|
||||||
|
}
|
||||||
|
_logger.info(f"KDS NOTIFICATION: Sending update for Line {line.id} Status {line.preparation_status} to {channel_name}")
|
||||||
|
self.env['bus.bus']._sendone(channel_name, 'kds_update', payload)
|
||||||
|
else:
|
||||||
|
_logger.warning(f"Line {line.id} has no config_id - cannot send notification")
|
||||||
|
|
||||||
|
def _notify_kds(self):
|
||||||
|
"""Send notification to KDS backend when new order lines are created"""
|
||||||
|
_logger.info("=== _notify_kds called for %s lines ===" % len(self))
|
||||||
|
for line in self:
|
||||||
|
# Only notify for kitchen items
|
||||||
|
if line.product_id.is_kitchen_item and line.product_id.name != 'Water':
|
||||||
|
# Send to global KDS channel
|
||||||
|
kds_channel = "kds_channel"
|
||||||
|
payload = {
|
||||||
|
'line_id': line.id,
|
||||||
|
'order_id': line.order_id.id,
|
||||||
|
'product_name': line.product_id.name,
|
||||||
|
'qty': line.qty,
|
||||||
|
'table_name': line.table_id.name if line.table_id else '',
|
||||||
|
'floor_name': line.floor_id.name if line.floor_id else '',
|
||||||
|
'customer_note': line.customer_note or '',
|
||||||
|
'preparation_status': line.preparation_status,
|
||||||
|
'create_date': line.create_date.isoformat() if line.create_date else '',
|
||||||
|
}
|
||||||
|
_logger.info(f"KDS BACKEND NOTIFICATION: New order line {line.id} for {line.product_id.name}")
|
||||||
|
self.env['bus.bus']._sendone(kds_channel, 'kds_new_order', payload)
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
"""Override create to send notifications to KDS when new orders are added"""
|
||||||
|
lines = super(PosOrderLine, self).create(vals_list)
|
||||||
|
# Skip KDS notification if flagged (online orders wait for cashier confirmation)
|
||||||
|
if not self.env.context.get('skip_kds_notify'):
|
||||||
|
# Send notification to KDS backend only for new items (waiting status)
|
||||||
|
waiting_lines = lines.filtered(lambda l: l.preparation_status == 'waiting')
|
||||||
|
if waiting_lines:
|
||||||
|
waiting_lines._notify_kds()
|
||||||
|
return lines
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
"""Prevent resetting 'served' or 'ready' status back to 'waiting' during POS sync"""
|
||||||
|
if 'preparation_status' in vals and vals['preparation_status'] == 'waiting':
|
||||||
|
for line in self:
|
||||||
|
if line.preparation_status in ['served', 'ready', 'preparing']:
|
||||||
|
# Keep the current status if it's already being processed or served
|
||||||
|
actual_vals = vals.copy()
|
||||||
|
actual_vals['preparation_status'] = line.preparation_status
|
||||||
|
super(PosOrderLine, line).write(actual_vals)
|
||||||
|
|
||||||
|
# Handle lines that are actually allowed to be updated to waiting
|
||||||
|
remaining_lines = self.filtered(lambda l: l.preparation_status not in ['served', 'ready', 'preparing'])
|
||||||
|
if remaining_lines:
|
||||||
|
res = super(PosOrderLine, remaining_lines).write(vals)
|
||||||
|
# If quantity changed or status is waiting, notify KDS to refresh
|
||||||
|
remaining_lines._notify_kds()
|
||||||
|
return res
|
||||||
|
return True
|
||||||
|
|
||||||
|
res = super(PosOrderLine, self).write(vals)
|
||||||
|
# Notify KDS for quantity changes on active items
|
||||||
|
if 'qty' in vals:
|
||||||
|
active_lines = self.filtered(lambda l: l.preparation_status in ['waiting', 'preparing'])
|
||||||
|
if active_lines:
|
||||||
|
active_lines._notify_kds()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def action_start_preparing(self):
|
||||||
|
self.write({
|
||||||
|
'preparation_status': 'preparing',
|
||||||
|
'preparation_time_start': fields.Datetime.now()
|
||||||
|
})
|
||||||
|
self._notify_pos()
|
||||||
|
|
||||||
|
def action_mark_ready(self):
|
||||||
|
self.write({
|
||||||
|
'preparation_status': 'ready',
|
||||||
|
'preparation_time_end': fields.Datetime.now()
|
||||||
|
})
|
||||||
|
self._notify_pos()
|
||||||
|
|
||||||
|
def action_mark_served(self):
|
||||||
|
self.write({
|
||||||
|
'preparation_status': 'served'
|
||||||
|
})
|
||||||
|
self._notify_pos()
|
||||||
|
|
||||||
|
class PosOrder(models.Model):
|
||||||
|
_inherit = 'pos.order'
|
||||||
|
|
||||||
|
order_source = fields.Selection([
|
||||||
|
('walk_in', 'Walk-In (Standard POS)'),
|
||||||
|
('phone', 'Telephone Order'),
|
||||||
|
('online', 'Online / eCommerce'),
|
||||||
|
('whatsapp', 'WhatsApp'),
|
||||||
|
('social_media', 'Social Media'),
|
||||||
|
('platform', 'Third-Party Platform'),
|
||||||
|
('kiosk', 'Self-Order Kiosk'),
|
||||||
|
('qr', 'QR Table Order'),
|
||||||
|
], string='Order Source', default='walk_in')
|
||||||
|
|
||||||
|
fulfilment_type = fields.Selection([
|
||||||
|
('dine_in', 'Dine-In'),
|
||||||
|
('pickup', 'Pickup'),
|
||||||
|
('delivery', 'Delivery'),
|
||||||
|
], string='Fulfilment Type', default='dine_in')
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _prepare_order_line_vals(self, line, session_id=None):
|
||||||
|
res = super()._prepare_order_line_vals(line, session_id)
|
||||||
|
if 'preparation_status' in line:
|
||||||
|
res['preparation_status'] = line['preparation_status']
|
||||||
|
return res
|
||||||
9
addons/dine360_kds/models/pos_session.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from odoo import models
|
||||||
|
|
||||||
|
class PosSession(models.Model):
|
||||||
|
_inherit = 'pos.session'
|
||||||
|
|
||||||
|
def _loader_params_pos_order_line(self):
|
||||||
|
params = super()._loader_params_pos_order_line()
|
||||||
|
params['search_params']['fields'].extend(['preparation_status', 'preparation_time_start', 'preparation_time_end'])
|
||||||
|
return params
|
||||||
10
addons/dine360_kds/models/product.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from odoo import models, fields
|
||||||
|
|
||||||
|
class ProductTemplate(models.Model):
|
||||||
|
_inherit = 'product.template'
|
||||||
|
|
||||||
|
is_kitchen_item = fields.Boolean(
|
||||||
|
string='Show in KDS',
|
||||||
|
default=False,
|
||||||
|
help="If checked, this product will appear in the Kitchen Display System when ordered."
|
||||||
|
)
|
||||||
105
addons/dine360_kds/models/website_sale_integration.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
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, but allow delivery lines for accurate total matching
|
||||||
|
is_delivery_line = getattr(line, 'is_delivery', False)
|
||||||
|
if not is_delivery_line and (not line.product_id.is_kitchen_item or line.product_id.type == 'service'):
|
||||||
|
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)],
|
||||||
|
# Online orders: hold for cashier confirmation before sending to KDS
|
||||||
|
'preparation_status': False,
|
||||||
|
'customer_note': 'Web Order',
|
||||||
|
}))
|
||||||
|
|
||||||
|
if not lines_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Generate proper POS reference matching Odoo's regex pattern '([0-9-]){14,}'
|
||||||
|
# Odoo's _export_for_ui expects this to exist otherwise it crashes
|
||||||
|
import datetime
|
||||||
|
uid = f"{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}-{session.id}-{sale_order.id}"
|
||||||
|
pos_reference = f"Order {uid}"
|
||||||
|
|
||||||
|
# 4. Create POS Order (in Draft/New state to avoid double accounting)
|
||||||
|
# Use skip_kds_notify context to prevent immediate KDS notification
|
||||||
|
# Online orders will be sent to KDS only after cashier confirmation
|
||||||
|
pos_order = PosOrder.with_context(skip_kds_notify=True).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
|
||||||
|
})
|
||||||
|
|
||||||
|
# Notification to KDS is deferred until cashier confirms via dine360_online_orders
|
||||||
|
_logger.info(f"Created POS Order {pos_order.name} from Website Order {sale_order.name} (pending cashier confirmation).")
|
||||||
6
addons/dine360_kds/security/ir.model.access.csv
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_kds_order_line_kitchen,pos.order.line.kitchen,point_of_sale.model_pos_order_line,dine360_restaurant.group_restaurant_kitchen,1,1,0,0
|
||||||
|
access_kds_order_line_manager,pos.order.line.manager,point_of_sale.model_pos_order_line,dine360_restaurant.group_restaurant_manager,1,1,1,1
|
||||||
|
access_kds_order_line_user,pos.order.line.user,point_of_sale.model_pos_order_line,base.group_user,1,1,1,0
|
||||||
|
access_kds_pos_session_kitchen,pos.session.kitchen,point_of_sale.model_pos_session,dine360_restaurant.group_restaurant_kitchen,1,0,0,0
|
||||||
|
access_kds_pos_category_kitchen,pos.category.kitchen,point_of_sale.model_pos_category,dine360_restaurant.group_restaurant_kitchen,1,0,0,0
|
||||||
|
BIN
addons/dine360_kds/static/description/icon.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
49
addons/dine360_kds/static/src/css/kds_style.css
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
.role_kitchen_card {
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
cursor: default !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role_kitchen_card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_kanban_group {
|
||||||
|
background-color: #f4f7fa !important;
|
||||||
|
border-radius: 15px !important;
|
||||||
|
margin: 10px !important;
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_kanban_header {
|
||||||
|
background: transparent !important;
|
||||||
|
padding: 15px 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_kanban_header_title {
|
||||||
|
font-weight: 700 !important;
|
||||||
|
text-transform: uppercase !important;
|
||||||
|
letter-spacing: 1px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specific colors for status columns */
|
||||||
|
.o_kanban_group[data-id="waiting"] .o_kanban_header_title {
|
||||||
|
color: #f0ad4e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_kanban_group[data-id="preparing"] .o_kanban_header_title {
|
||||||
|
color: #5bc0de;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_kanban_group[data-id="ready"] .o_kanban_header_title {
|
||||||
|
color: #5cb85c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_kanban_group[data-id="served"] .o_kanban_header_title {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.rounded-pill {
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
29
addons/dine360_kds/static/src/css/pos_kds.css
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
.pos .badge {
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
margin-left: 5px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .badge-waiting {
|
||||||
|
background-color: #f0ad4e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .badge-preparing {
|
||||||
|
background-color: #5bc0de;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .badge-ready {
|
||||||
|
background-color: #5cb85c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .badge-served {
|
||||||
|
background-color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos .badge-cancelled {
|
||||||
|
background-color: #d9534f;
|
||||||
|
}
|
||||||
91
addons/dine360_kds/static/src/js/kds_backend.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
/** @odoo-module */
|
||||||
|
|
||||||
|
import { registry } from "@web/core/registry";
|
||||||
|
import { KanbanController } from "@web/views/kanban/kanban_controller";
|
||||||
|
import { kanbanView } from "@web/views/kanban/kanban_view";
|
||||||
|
import { onWillUnmount } from "@odoo/owl";
|
||||||
|
import { useService } from "@web/core/utils/hooks";
|
||||||
|
|
||||||
|
export class KdsKanbanController extends KanbanController {
|
||||||
|
setup() {
|
||||||
|
super.setup();
|
||||||
|
console.log("[KDS Controller] Setup");
|
||||||
|
|
||||||
|
// Direct access to services to avoid 'methods is not iterable' error in Owl lifecycle
|
||||||
|
this.busService = this.env.services.bus_service;
|
||||||
|
this.notification = this.env.services.notification;
|
||||||
|
|
||||||
|
const kdsChannel = "kds_channel";
|
||||||
|
|
||||||
|
if (this.busService) {
|
||||||
|
console.log(`[KDS Controller] Subscribing to channel: ${kdsChannel}`);
|
||||||
|
this.busService.addChannel(kdsChannel);
|
||||||
|
|
||||||
|
const handler = this._onKdsNotification.bind(this);
|
||||||
|
this.busService.addEventListener("notification", handler);
|
||||||
|
|
||||||
|
onWillUnmount(() => {
|
||||||
|
if (this.busService) {
|
||||||
|
this.busService.removeEventListener("notification", handler);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onKdsNotification(event) {
|
||||||
|
// console.log("[KDS Controller] Notification received:", event);
|
||||||
|
|
||||||
|
const notifications = event.detail || [];
|
||||||
|
let shouldReload = false;
|
||||||
|
|
||||||
|
for (const notif of notifications) {
|
||||||
|
// console.log("[KDS Controller] Processing notification:", notif);
|
||||||
|
|
||||||
|
if (notif.type === "kds_new_order") {
|
||||||
|
console.log("[KDS Controller] New order notification:", notif.payload);
|
||||||
|
|
||||||
|
// Show notification to user
|
||||||
|
if (this.notification) {
|
||||||
|
const payload = notif.payload;
|
||||||
|
this.notification.add(
|
||||||
|
`New Order: ${payload.qty}x ${payload.product_name} - ${payload.table_name || 'Web/Takeaway'}`,
|
||||||
|
{
|
||||||
|
title: "Kitchen Display",
|
||||||
|
type: "info",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldReload = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldReload) {
|
||||||
|
// Reload the view to show the new order
|
||||||
|
// Adding a small delay to ensure the transaction is fully committed and indexed
|
||||||
|
console.log("[KDS Controller] New order received. Reloading view in 1.5s...");
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
await this.model.load();
|
||||||
|
console.log("[KDS Controller] View reloaded. Current record count:", this.model.root.count);
|
||||||
|
|
||||||
|
// Force a re-render if the view still thinks it's empty
|
||||||
|
if (this.model.root.count > 0) {
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[KDS Controller] Error during reload:", error);
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const kdsKanbanView = {
|
||||||
|
...kanbanView,
|
||||||
|
Controller: KdsKanbanController,
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.category("views").add("kds_kanban", kdsKanbanView);
|
||||||
|
|
||||||
|
console.log("[KDS Backend] kds_backend.js loaded (JS Class mode - manual services)");
|
||||||
133
addons/dine360_kds/static/src/js/pos_kds.js
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
/** @odoo-module */
|
||||||
|
|
||||||
|
// Immediate console log to verify file is loaded
|
||||||
|
console.log("==============================================");
|
||||||
|
console.log("[KDS] pos_kds.js FILE IS LOADING!");
|
||||||
|
console.log("==============================================");
|
||||||
|
|
||||||
|
import { Orderline } from "@point_of_sale/app/store/models";
|
||||||
|
import { PosStore } from "@point_of_sale/app/store/pos_store";
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
|
||||||
|
console.log("[KDS] Imports successful");
|
||||||
|
|
||||||
|
// Patch Orderline model
|
||||||
|
patch(Orderline.prototype, {
|
||||||
|
setup(attr) {
|
||||||
|
super.setup(...arguments);
|
||||||
|
if (attr && attr.preparation_status) {
|
||||||
|
this.preparation_status = attr.preparation_status;
|
||||||
|
} else if (!this.preparation_status) {
|
||||||
|
this.preparation_status = 'waiting';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
init_from_JSON(json) {
|
||||||
|
super.init_from_JSON(...arguments);
|
||||||
|
if (json.preparation_status) {
|
||||||
|
this.preparation_status = json.preparation_status;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
export_as_JSON() {
|
||||||
|
const json = super.export_as_JSON(...arguments);
|
||||||
|
json.preparation_status = this.preparation_status;
|
||||||
|
return json;
|
||||||
|
},
|
||||||
|
|
||||||
|
set_preparation_status(status) {
|
||||||
|
this.preparation_status = status;
|
||||||
|
},
|
||||||
|
|
||||||
|
get_preparation_status() {
|
||||||
|
return this.preparation_status;
|
||||||
|
},
|
||||||
|
|
||||||
|
get_preparation_status_label() {
|
||||||
|
const labels = {
|
||||||
|
'waiting': 'Waiting',
|
||||||
|
'preparing': 'Preparing',
|
||||||
|
'ready': 'Ready',
|
||||||
|
'served': 'Served',
|
||||||
|
'cancelled': 'Cancelled'
|
||||||
|
};
|
||||||
|
return labels[this.preparation_status] || this.preparation_status;
|
||||||
|
},
|
||||||
|
|
||||||
|
can_be_merged_with(line) {
|
||||||
|
// Prevent merging if the existing line is already being prepared or served
|
||||||
|
if (this.preparation_status && this.preparation_status !== 'waiting') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return super.can_be_merged_with(...arguments);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[KDS] Orderline patched successfully");
|
||||||
|
|
||||||
|
// Patch PosStore
|
||||||
|
patch(PosStore.prototype, {
|
||||||
|
async _processData(loadedData) {
|
||||||
|
console.log("[KDS] _processData called!");
|
||||||
|
await super._processData(...arguments);
|
||||||
|
|
||||||
|
const channel = `pos_config_Channel_${this.config.id}`;
|
||||||
|
console.log(`[KDS] Setting up channel: ${channel}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const busService = this.env.services.bus_service;
|
||||||
|
busService.addChannel(channel);
|
||||||
|
console.log("[KDS] Channel added successfully");
|
||||||
|
|
||||||
|
busService.addEventListener("notification", (event) => {
|
||||||
|
console.log("[KDS] *** NOTIFICATION RECEIVED ***", event);
|
||||||
|
const notifications = event.detail || [];
|
||||||
|
|
||||||
|
for (const notif of notifications) {
|
||||||
|
if (notif.type === 'kds_update') {
|
||||||
|
console.log("[KDS] *** KDS UPDATE ***", notif.payload);
|
||||||
|
this._handleKdsUpdate(notif.payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[KDS] Listener registered successfully");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[KDS] ERROR:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_handleKdsUpdate(payload) {
|
||||||
|
console.log("[KDS] Handling update:", payload);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { order_uid, product_id, status, status_label, line_id, order_id } = payload;
|
||||||
|
const orders = this.get_order_list();
|
||||||
|
|
||||||
|
let order = orders.find(o => o.server_id === order_id);
|
||||||
|
if (!order) {
|
||||||
|
order = orders.find(o => o.name === order_uid || o.pos_reference === order_uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (order) {
|
||||||
|
let line = order.get_orderlines().find(l => l.server_id === line_id);
|
||||||
|
if (!line) {
|
||||||
|
line = order.get_orderlines().find(l => l.product.id === product_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line) {
|
||||||
|
line.set_preparation_status(status);
|
||||||
|
this.env.services.notification.add(
|
||||||
|
`${line.product.display_name} is ${status_label}`,
|
||||||
|
{ title: "Kitchen Update", type: "info" }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[KDS] Error in handler:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[KDS] PosStore patched successfully");
|
||||||
|
console.log("[KDS] MODULE FULLY LOADED!");
|
||||||
12
addons/dine360_kds/static/src/xml/pos_kds.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
<t t-name="dine360_kds.Orderline" t-inherit="point_of_sale.Orderline" t-inherit-mode="extension" owl="1">
|
||||||
|
<xpath expr="//ul[hasclass('info-list')]" position="inside">
|
||||||
|
<li class="info" t-if="props.line.get_preparation_status() and props.line.get_preparation_status() !== 'waiting'">
|
||||||
|
<span t-attf-class="badge badge-{{props.line.get_preparation_status()}}">
|
||||||
|
<t t-esc="props.line.get_preparation_status_label()"/>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</xpath>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
14
addons/dine360_kds/views/kds_menus.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<odoo>
|
||||||
|
<menuitem id="menu_kds_root"
|
||||||
|
name="Kitchen (KDS)"
|
||||||
|
web_icon="dine360_kds,static/description/icon.png"
|
||||||
|
groups="base.group_user"
|
||||||
|
sequence="20"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_kds_orders"
|
||||||
|
name="Preparation Orders"
|
||||||
|
parent="menu_kds_root"
|
||||||
|
action="action_kds_dashboard"
|
||||||
|
groups="base.group_user"
|
||||||
|
sequence="10"/>
|
||||||
|
</odoo>
|
||||||
150
addons/dine360_kds/views/pos_order_line_views.xml
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
<odoo>
|
||||||
|
<!-- KDS Kanban View -->
|
||||||
|
<record id="view_pos_order_line_kds_kanban" model="ir.ui.view">
|
||||||
|
<field name="name">pos.order.line.kds.kanban</field>
|
||||||
|
<field name="model">pos.order.line</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<kanban js_class="kds_kanban" default_group_by="preparation_status" create="false" class="o_kanban_small_column o_kanban_project_tasks">
|
||||||
|
<field name="preparation_status"/>
|
||||||
|
<field name="color"/>
|
||||||
|
<field name="product_id"/>
|
||||||
|
<field name="qty"/>
|
||||||
|
<field name="order_id"/>
|
||||||
|
<field name="table_id"/>
|
||||||
|
<field name="floor_id"/>
|
||||||
|
<field name="order_source"/>
|
||||||
|
<field name="fulfilment_type"/>
|
||||||
|
<field name="customer_note"/>
|
||||||
|
<field name="create_date"/>
|
||||||
|
<templates>
|
||||||
|
<t t-name="kanban-box">
|
||||||
|
<div t-attf-class="oe_kanban_global_click role_kitchen_card #{'oe_kanban_color_' + kanban_getcolor(record.color.raw_value)} shadow-sm border-0 mb-3" style="border-radius: 12px; border-left: 5px solid #fecd4f !important;">
|
||||||
|
<div class="oe_kanban_content p-3">
|
||||||
|
<div class="o_kanban_record_top mb-2">
|
||||||
|
<div class="o_kanban_record_headings">
|
||||||
|
<strong class="o_kanban_record_title" style="font-size: 1.2rem; color: #171422;">
|
||||||
|
<field name="qty"/> x <field name="product_id"/>
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
<div class="ms-auto h5 mb-0">
|
||||||
|
<span t-if="record.table_id.raw_value" class="badge rounded-pill bg-light text-dark border">
|
||||||
|
<i class="fa fa-map-marker me-1" title="Table"/> <field name="table_id"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<t t-if="record.customer_note.raw_value">
|
||||||
|
<div class="alert alert-warning py-2 px-3 mb-3 border-0" role="status" style="background: rgba(254, 205, 79, 0.1); border-radius: 8px;">
|
||||||
|
<i class="fa fa-sticky-note-o me-2" title="Note"/> <strong>Note:</strong> <field name="customer_note"/>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<div class="o_kanban_record_body small text-muted mb-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||||
|
<span><i class="fa fa-clock-o me-1" title="Time"/> <field name="create_date"/></span>
|
||||||
|
<span class="text-uppercase fw-bold" style="font-size: 0.7rem;"><field name="floor_id"/></span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<span t-if="record.order_source.raw_value" class="badge bg-info-light text-info border-info" style="font-size: 0.65rem; background: rgba(23, 162, 184, 0.1);">
|
||||||
|
<i class="fa fa-plug me-1" title="Source"/> <field name="order_source"/>
|
||||||
|
</span>
|
||||||
|
<span t-if="record.fulfilment_type.raw_value" class="badge bg-warning-light text-warning border-warning" style="font-size: 0.65rem; background: rgba(254, 205, 79, 0.1);">
|
||||||
|
<i class="fa fa-truck me-1" title="Fulfilment"/> <field name="fulfilment_type"/>
|
||||||
|
</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="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'"
|
||||||
|
name="action_start_preparing" type="object"
|
||||||
|
class="btn btn-sm btn-primary px-3 shadow-sm" style="background: #171422; border: none; border-radius: 8px;">
|
||||||
|
Start Cooking
|
||||||
|
</button>
|
||||||
|
<button t-if="record.preparation_status.raw_value == 'preparing'"
|
||||||
|
name="action_mark_ready" type="object"
|
||||||
|
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' 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
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
|
</kanban>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- KDS Tree View -->
|
||||||
|
<record id="view_pos_order_line_kds_tree" model="ir.ui.view">
|
||||||
|
<field name="name">pos.order.line.kds.tree</field>
|
||||||
|
<field name="model">pos.order.line</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree string="Kitchen Orders" create="false" edit="false" decoration-info="preparation_status == 'preparing'" decoration-success="preparation_status == 'ready'" decoration-muted="preparation_status == 'served'">
|
||||||
|
<field name="order_id"/>
|
||||||
|
<field name="order_source" widget="badge"/>
|
||||||
|
<field name="fulfilment_type" widget="badge"/>
|
||||||
|
<field name="floor_id"/>
|
||||||
|
<field name="table_id"/>
|
||||||
|
<field name="product_id"/>
|
||||||
|
<field name="qty"/>
|
||||||
|
<field name="customer_note" string="Notes"/>
|
||||||
|
<field name="preparation_status" widget="badge" decoration-info="preparation_status == 'preparing'" decoration-success="preparation_status == 'ready'" decoration-warning="preparation_status == 'waiting'"/>
|
||||||
|
<field name="create_date" string="Ordered At" widget="remaining_days"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- KDS Search View -->
|
||||||
|
<record id="view_pos_order_line_kds_search" model="ir.ui.view">
|
||||||
|
<field name="name">pos.order.line.kds.search</field>
|
||||||
|
<field name="model">pos.order.line</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Kitchen Orders Search">
|
||||||
|
<field name="order_id"/>
|
||||||
|
<field name="product_id"/>
|
||||||
|
<field name="table_id"/>
|
||||||
|
<field name="floor_id"/>
|
||||||
|
<separator/>
|
||||||
|
<filter string="Waiting" name="waiting" domain="[('preparation_status', '=', 'waiting')]"/>
|
||||||
|
<filter string="Preparing" name="preparing" domain="[('preparation_status', '=', 'preparing')]"/>
|
||||||
|
<filter string="Ready" name="ready" domain="[('preparation_status', '=', 'ready')]"/>
|
||||||
|
<filter string="In Progress" name="in_progress" domain="[('preparation_status', 'in', ['waiting', 'preparing'])]"/>
|
||||||
|
<separator/>
|
||||||
|
<filter string="Today" name="today" domain="[('create_date', '>=', context_today().strftime('%Y-%m-%d 00:00:00'))]"/>
|
||||||
|
<group expand="0" string="Group By">
|
||||||
|
<filter string="Status" name="group_status" context="{'group_by': 'preparation_status'}"/>
|
||||||
|
<filter string="Table" name="group_table" context="{'group_by': 'table_id'}"/>
|
||||||
|
<filter string="Floor" name="group_floor" context="{'group_by': 'floor_id'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Window Action -->
|
||||||
|
<record id="action_kds_dashboard" model="ir.actions.act_window">
|
||||||
|
<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', '=', 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">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Welcome to the Kitchen!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Orders sent from the POS will appear here for preparation.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
13
addons/dine360_kds/views/product_views.xml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="product_template_form_view_kds" model="ir.ui.view">
|
||||||
|
<field name="name">product.template.form.kds</field>
|
||||||
|
<field name="model">product.template</field>
|
||||||
|
<field name="inherit_id" ref="product.product_template_only_form_view"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='categ_id']" position="after">
|
||||||
|
<field name="is_kitchen_item"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
2
addons/dine360_online_orders/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from . import models
|
||||||
|
from . import controllers
|
||||||
32
addons/dine360_online_orders/__manifest__.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
'name': 'Dine360 Online Orders in POS',
|
||||||
|
'version': '17.0.1.0',
|
||||||
|
'category': 'Sales/Point of Sale',
|
||||||
|
'summary': 'Receive website shop orders on POS screen with KDS integration',
|
||||||
|
'description': """
|
||||||
|
Online Orders Integration for POS:
|
||||||
|
- Website shop orders appear in POS as a new 'Online Orders' tab
|
||||||
|
- Cashier can confirm or reject online orders
|
||||||
|
- Confirmed orders are sent to Kitchen Display System (KDS)
|
||||||
|
- Real-time notifications via bus service
|
||||||
|
""",
|
||||||
|
'author': 'Dine360',
|
||||||
|
'depends': ['point_of_sale', 'pos_restaurant', 'dine360_kds', 'website_sale', 'sale_management', 'dine360_order_channels'],
|
||||||
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'views/pos_order_views.xml',
|
||||||
|
'views/kds_override_views.xml',
|
||||||
|
'views/pos_config_views.xml',
|
||||||
|
'views/website_sale_templates.xml',
|
||||||
|
'views/backend_online_orders.xml',
|
||||||
|
],
|
||||||
|
'assets': {
|
||||||
|
'web.assets_frontend': [
|
||||||
|
'dine360_online_orders/static/src/css/service_mode.css',
|
||||||
|
'dine360_online_orders/static/src/js/service_mode.js',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'installable': True,
|
||||||
|
'application': False,
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
1
addons/dine360_online_orders/controllers/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import main
|
||||||
65
addons/dine360_online_orders/controllers/main.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
from odoo import http
|
||||||
|
from odoo.http import request
|
||||||
|
from odoo.addons.website_sale.controllers.main import WebsiteSale
|
||||||
|
|
||||||
|
class Dine360OnlineOrders(http.Controller):
|
||||||
|
|
||||||
|
@http.route('/shop/update_service_mode', type='json', auth="public", website=True)
|
||||||
|
def update_service_mode(self, service_mode, **post):
|
||||||
|
order = request.website.sale_get_order()
|
||||||
|
if order and service_mode in ['pickup', 'delivery', 'dine_in']:
|
||||||
|
order.sudo().write({
|
||||||
|
'fulfilment_type': service_mode,
|
||||||
|
'order_source': 'online'
|
||||||
|
})
|
||||||
|
return True
|
||||||
|
|
||||||
|
class Dine360WebsiteSaleOnline(WebsiteSale):
|
||||||
|
@http.route(['/shop/payment'], type='http', auth="public", website=True, sitemap=False)
|
||||||
|
def shop_payment(self, **post):
|
||||||
|
order = request.website.sale_get_order()
|
||||||
|
if order and not order.carrier_id:
|
||||||
|
# Bypass delivery validation by assigning the first available delivery carrier
|
||||||
|
carriers = order._get_delivery_methods()
|
||||||
|
if not carriers:
|
||||||
|
carriers = request.env['delivery.carrier'].sudo().search([('is_published', '=', True)], limit=1)
|
||||||
|
|
||||||
|
if carriers:
|
||||||
|
order.carrier_id = carriers[0].id
|
||||||
|
price = carriers[0].rate_shipment(order)['price'] if hasattr(carriers[0], 'rate_shipment') and order._get_delivery_methods() else 0.0
|
||||||
|
order.set_delivery_line(carriers[0], price)
|
||||||
|
|
||||||
|
return super(Dine360WebsiteSaleOnline, self).shop_payment(**post)
|
||||||
|
|
||||||
|
@http.route('/shop/payment/transaction/<int:order_id>', type='json', auth="public", website=True)
|
||||||
|
def shop_payment_transaction(self, order_id, access_token=None, **kwargs):
|
||||||
|
# Force bypass "No shipping method is selected" error during payment processing
|
||||||
|
order = request.env['sale.order'].sudo().browse(order_id)
|
||||||
|
if order and not order.carrier_id:
|
||||||
|
carriers = request.env['delivery.carrier'].sudo().search([('is_published', '=', True)], limit=1)
|
||||||
|
if carriers:
|
||||||
|
order.carrier_id = carriers[0].id
|
||||||
|
order.set_delivery_line(carriers[0], 0.0)
|
||||||
|
return super(Dine360WebsiteSaleOnline, self).shop_payment_transaction(order_id, access_token=access_token, **kwargs)
|
||||||
|
|
||||||
|
@http.route(['/shop/address'], type='http', methods=['GET', 'POST'], auth="public", website=True, sitemap=False)
|
||||||
|
def address(self, **post):
|
||||||
|
# Override to inject dummy address values for pickup orders before Odoo validates them
|
||||||
|
if post.get('submitted') and post.get('fulfilment_type') == 'pickup':
|
||||||
|
if not post.get('street'): post['street'] = 'Store Pickup'
|
||||||
|
if not post.get('city'): post['city'] = 'Local'
|
||||||
|
if not post.get('zip'): post['zip'] = '00000'
|
||||||
|
if not post.get('country_id'):
|
||||||
|
country = request.env['res.country'].sudo().search([], limit=1)
|
||||||
|
post['country_id'] = str(country.id) if country else ''
|
||||||
|
|
||||||
|
# Request.params must also be updated for Odoo 17 validation
|
||||||
|
for k, v in post.items():
|
||||||
|
if k in ['street', 'city', 'zip', 'country_id']:
|
||||||
|
if hasattr(request.params, 'update'):
|
||||||
|
try:
|
||||||
|
request.params[k] = v
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return super(Dine360WebsiteSaleOnline, self).address(**post)
|
||||||
5
addons/dine360_online_orders/models/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from . import pos_order
|
||||||
|
from . import sale_order
|
||||||
|
from . import pos_config
|
||||||
|
from . import res_config_settings
|
||||||
|
from . import pos_order_line
|
||||||
10
addons/dine360_online_orders/models/pos_config.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from odoo import models, fields
|
||||||
|
|
||||||
|
class PosConfig(models.Model):
|
||||||
|
_inherit = 'pos.config'
|
||||||
|
|
||||||
|
is_kiosk = fields.Boolean(string='Is Self-Order Kiosk', default=False)
|
||||||
|
kiosk_service_mode = fields.Selection([
|
||||||
|
('pickup', 'Pickup'),
|
||||||
|
('dine_in', 'Dine-In')
|
||||||
|
], string='Default Kiosk Service Mode', default='dine_in')
|
||||||
206
addons/dine360_online_orders/models/pos_order.py
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
from odoo import models, fields, api, _
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PosOrder(models.Model):
|
||||||
|
_inherit = 'pos.order'
|
||||||
|
|
||||||
|
is_online_order = fields.Boolean(
|
||||||
|
string='Online Order',
|
||||||
|
default=False,
|
||||||
|
help='Indicates this order came from the website shop'
|
||||||
|
)
|
||||||
|
online_order_status = fields.Selection([
|
||||||
|
('pending', 'Pending Kitchen'),
|
||||||
|
('confirmed', 'Confirmed'),
|
||||||
|
('rejected', 'Rejected'),
|
||||||
|
], string='Online Order Status', default='pending')
|
||||||
|
|
||||||
|
sale_order_id = fields.Many2one(
|
||||||
|
'sale.order', string='Source Sale Order',
|
||||||
|
help='The website sale order that generated this POS order'
|
||||||
|
)
|
||||||
|
online_customer_name = fields.Char(
|
||||||
|
string='Online Customer',
|
||||||
|
compute='_compute_online_customer_name', store=True
|
||||||
|
)
|
||||||
|
online_order_date = fields.Datetime(
|
||||||
|
string='Online Order Date',
|
||||||
|
default=fields.Datetime.now
|
||||||
|
)
|
||||||
|
# delivery_time = fields.Datetime(string='Requested Delivery Time')
|
||||||
|
|
||||||
|
# Note: order_source and fulfilment_type fields are defined in dine360_order_channels
|
||||||
|
# dine360_online_orders just uses these fields
|
||||||
|
|
||||||
|
@api.depends('partner_id', 'partner_id.name')
|
||||||
|
def _compute_online_customer_name(self):
|
||||||
|
for order in self:
|
||||||
|
order.online_customer_name = order.partner_id.name or 'Guest'
|
||||||
|
|
||||||
|
def action_confirm_online_order(self):
|
||||||
|
"""Cashier confirms the online order → sends to KDS and marks as paid if already paid online"""
|
||||||
|
self.ensure_one()
|
||||||
|
self.write({'online_order_status': 'confirmed'})
|
||||||
|
|
||||||
|
# If it's an online order with an online payment option or Stripe txn, mark as paid in POS to avoid confusion
|
||||||
|
has_paid_transaction = False
|
||||||
|
if self.sale_order_id:
|
||||||
|
has_paid_transaction = any(t.state in ['authorized', 'done'] for t in self.sale_order_id.transaction_ids)
|
||||||
|
|
||||||
|
if self.is_online_order and self.sale_order_id and (self.sale_order_id.payment_option == 'online_gateway' or has_paid_transaction):
|
||||||
|
# Check if it needs payment (not yet paid in POS)
|
||||||
|
if self.state == 'draft' and self.amount_total > 0 and self.amount_paid < self.amount_total:
|
||||||
|
# Find a suitable payment method (Online Payment or Stripe)
|
||||||
|
# We prioritize methods linked to the current POS config
|
||||||
|
payment_method = self._get_online_payment_method()
|
||||||
|
|
||||||
|
if payment_method:
|
||||||
|
_logger.info("Automatically adding online payment from Stripe gateway for order %s using method %s", self.name, payment_method.name)
|
||||||
|
|
||||||
|
# Use add_payment if it exists, otherwise manual creation
|
||||||
|
payment_data = {
|
||||||
|
'amount': self.amount_total,
|
||||||
|
'payment_date': fields.Datetime.now(),
|
||||||
|
'payment_method_id': payment_method.id,
|
||||||
|
'pos_order_id': self.id,
|
||||||
|
}
|
||||||
|
if hasattr(self, 'add_payment'):
|
||||||
|
self.add_payment(payment_data)
|
||||||
|
else:
|
||||||
|
self.env['pos.payment'].create(payment_data)
|
||||||
|
|
||||||
|
# Force recomputation of amount_paid
|
||||||
|
self.env.flush_all()
|
||||||
|
self.invalidate_recordset(['payment_ids', 'amount_paid'])
|
||||||
|
|
||||||
|
# Instead of relying strictly on action_pos_order_paid which throws UserError on cache lag,
|
||||||
|
# we force the state logic directly if we just paid the exact full amount.
|
||||||
|
self.write({'state': 'paid'})
|
||||||
|
try:
|
||||||
|
self._create_order_picking()
|
||||||
|
except AttributeError:
|
||||||
|
_logger.warning("No _create_order_picking method found on POS order")
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error("Error creating picking for online POS order: %s", str(e))
|
||||||
|
else:
|
||||||
|
_logger.warning("Could not find a suitable POS Payment Method for online order %s", self.name)
|
||||||
|
|
||||||
|
# Set all order lines to 'waiting' so KDS picks them up
|
||||||
|
for line in self.lines:
|
||||||
|
if line.product_id.is_kitchen_item and line.product_id.type != 'service':
|
||||||
|
line.write({
|
||||||
|
'preparation_status': 'waiting',
|
||||||
|
})
|
||||||
|
|
||||||
|
# Notify KDS
|
||||||
|
self.lines.filtered(
|
||||||
|
lambda l: l.product_id.is_kitchen_item and l.product_id.type != 'service'
|
||||||
|
)._notify_kds()
|
||||||
|
|
||||||
|
# Notify POS that order was confirmed
|
||||||
|
if self.config_id:
|
||||||
|
channel = "online_orders_%s" % self.config_id.id
|
||||||
|
self.env['bus.bus']._sendone(channel, 'online_order_confirmed', {
|
||||||
|
'order_id': self.id,
|
||||||
|
'order_name': self.pos_reference or self.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
_logger.info("Online order %s confirmed and sent to KDS", self.name)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def action_reject_online_order(self):
|
||||||
|
"""Cashier rejects the online order"""
|
||||||
|
self.ensure_one()
|
||||||
|
self.write({'online_order_status': 'rejected'})
|
||||||
|
|
||||||
|
# Notify POS
|
||||||
|
if self.config_id:
|
||||||
|
channel = "online_orders_%s" % self.config_id.id
|
||||||
|
self.env['bus.bus']._sendone(channel, 'online_order_rejected', {
|
||||||
|
'order_id': self.id,
|
||||||
|
'order_name': self.pos_reference or self.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
_logger.info("Online order %s rejected", self.name)
|
||||||
|
return True
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
for vals in vals_list:
|
||||||
|
if 'session_id' in vals:
|
||||||
|
session = self.env['pos.session'].browse(vals['session_id'])
|
||||||
|
if session.config_id.is_kiosk:
|
||||||
|
vals['order_source'] = 'kiosk'
|
||||||
|
vals['fulfilment_type'] = session.config_id.kiosk_service_mode or 'pickup'
|
||||||
|
return super().create(vals_list)
|
||||||
|
|
||||||
|
def _get_online_payment_method(self):
|
||||||
|
"""Find a suitable POS payment method for online/stripe payments"""
|
||||||
|
# 1. Look for methods in the current config first
|
||||||
|
if self.config_id:
|
||||||
|
for method in self.config_id.payment_method_ids:
|
||||||
|
if 'online' in method.name.lower() or 'stripe' in method.name.lower():
|
||||||
|
return method
|
||||||
|
|
||||||
|
# Fallback to any non-cash method in config
|
||||||
|
for method in self.config_id.payment_method_ids:
|
||||||
|
if not method.is_cash_count:
|
||||||
|
return method
|
||||||
|
|
||||||
|
# 2. Global search if config search fails
|
||||||
|
method = self.env['pos.payment.method'].search([
|
||||||
|
('name', 'ilike', 'Online'),
|
||||||
|
], limit=1)
|
||||||
|
if not method:
|
||||||
|
method = self.env['pos.payment.method'].search([
|
||||||
|
('name', 'ilike', 'Stripe'),
|
||||||
|
], limit=1)
|
||||||
|
if not method:
|
||||||
|
method = self.env['pos.payment.method'].search([
|
||||||
|
('is_cash_count', '=', False)
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
return method
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def get_online_orders(self, config_id):
|
||||||
|
"""Fetch pending online orders for a specific POS config"""
|
||||||
|
domain = [
|
||||||
|
('is_online_order', '=', True),
|
||||||
|
('online_order_status', '=', 'pending'),
|
||||||
|
('config_id', '=', config_id),
|
||||||
|
]
|
||||||
|
orders = self.search(domain, order='online_order_date desc')
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for order in orders:
|
||||||
|
lines = []
|
||||||
|
for line in order.lines:
|
||||||
|
lines.append({
|
||||||
|
'id': line.id,
|
||||||
|
'product_name': line.full_product_name or line.product_id.name,
|
||||||
|
'qty': line.qty,
|
||||||
|
'price_unit': line.price_unit,
|
||||||
|
'price_subtotal_incl': line.price_subtotal_incl,
|
||||||
|
'customer_note': line.customer_note or '',
|
||||||
|
'is_kitchen_item': line.product_id.is_kitchen_item,
|
||||||
|
})
|
||||||
|
|
||||||
|
result.append({
|
||||||
|
'id': order.id,
|
||||||
|
'name': order.pos_reference or order.name,
|
||||||
|
'partner_name': order.partner_id.name or 'Guest',
|
||||||
|
'partner_phone': order.partner_id.phone or order.partner_id.mobile or '',
|
||||||
|
'amount_total': order.amount_total,
|
||||||
|
'date_order': order.date_order.isoformat() if order.date_order else '',
|
||||||
|
'sale_order_name': order.sale_order_id.name if order.sale_order_id else '',
|
||||||
|
'service_mode': order.fulfilment_type,
|
||||||
|
'order_source': order.order_source,
|
||||||
|
'note': order.note or '',
|
||||||
|
'lines': lines,
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
5
addons/dine360_online_orders/models/pos_order_line.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from odoo import models
|
||||||
|
|
||||||
|
class PosOrderLine(models.Model):
|
||||||
|
_inherit = 'pos.order.line'
|
||||||
|
# Related fields order_source and fulfilment_type are now provided by dine360_order_channels
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
from odoo import models, fields
|
||||||
|
|
||||||
|
class ResConfigSettings(models.TransientModel):
|
||||||
|
_inherit = 'res.config.settings'
|
||||||
|
|
||||||
|
is_kiosk = fields.Boolean(related='pos_config_id.is_kiosk', readonly=False)
|
||||||
|
kiosk_service_mode = fields.Selection(related='pos_config_id.kiosk_service_mode', readonly=False)
|
||||||
247
addons/dine360_online_orders/models/sale_order.py
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
from odoo import models, fields, api, _
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SaleOrderOnline(models.Model):
|
||||||
|
_inherit = 'sale.order'
|
||||||
|
|
||||||
|
pos_order_id = fields.Many2one(
|
||||||
|
'pos.order', string='POS Order',
|
||||||
|
help='The POS order created from this website sale order'
|
||||||
|
)
|
||||||
|
|
||||||
|
# order_source is now canonical field from dine360_order_channels (pos.order)
|
||||||
|
# We add it to sale.order for tracking which channel the web sale originated from
|
||||||
|
order_source = fields.Selection([
|
||||||
|
('online', 'Online'),
|
||||||
|
('phone', 'Phone'),
|
||||||
|
('whatsapp', 'WhatsApp'),
|
||||||
|
('social_media', 'Social Media'),
|
||||||
|
('in_person', 'In-Person (Walk-in/Dine-in)'),
|
||||||
|
('kiosk', 'Store Self-Order (Kiosk)'),
|
||||||
|
('party_order', 'Party Order'),
|
||||||
|
('platform_integration', 'Platform Integration (3rd Party)'),
|
||||||
|
], string='Order Source', default='online', tracking=True)
|
||||||
|
|
||||||
|
fulfilment_type = fields.Selection([
|
||||||
|
('pickup', 'Pickup'),
|
||||||
|
('delivery', 'Delivery'),
|
||||||
|
('dine_in', 'Dine-In'),
|
||||||
|
('walk_in', 'Walk-In'),
|
||||||
|
], string='Fulfillment Type', default='pickup', tracking=True)
|
||||||
|
|
||||||
|
def _has_deliverable_products(self):
|
||||||
|
""" Bypass Odoo's native delivery carrier validation for store pickup orders """
|
||||||
|
res = super(SaleOrderOnline, self)._has_deliverable_products()
|
||||||
|
if self.fulfilment_type == 'pickup':
|
||||||
|
return False
|
||||||
|
return res
|
||||||
|
|
||||||
|
online_order_status = fields.Selection([
|
||||||
|
('pending', 'Pending Kitchen'),
|
||||||
|
('confirmed', 'Confirmed'),
|
||||||
|
('rejected', 'Rejected'),
|
||||||
|
], string='Online Order Status', compute='_compute_online_order_status', store=True)
|
||||||
|
|
||||||
|
is_online_order_accepted_manually = fields.Boolean(string='Manually Accepted', default=False)
|
||||||
|
|
||||||
|
@api.depends('pos_order_id.online_order_status', 'state', 'order_source', 'is_online_order_accepted_manually')
|
||||||
|
def _compute_online_order_status(self):
|
||||||
|
for order in self:
|
||||||
|
if order.pos_order_id:
|
||||||
|
order.online_order_status = order.pos_order_id.online_order_status
|
||||||
|
elif order.state == 'cancel':
|
||||||
|
order.online_order_status = 'rejected'
|
||||||
|
elif order.order_source == 'online' and order.state in ['draft', 'sent', 'sale']:
|
||||||
|
if order.is_online_order_accepted_manually:
|
||||||
|
order.online_order_status = 'confirmed'
|
||||||
|
else:
|
||||||
|
order.online_order_status = 'pending'
|
||||||
|
else:
|
||||||
|
order.online_order_status = False
|
||||||
|
|
||||||
|
def action_accept_online_order(self):
|
||||||
|
for order in self:
|
||||||
|
if order.state in ['draft', 'sent']:
|
||||||
|
order.action_confirm()
|
||||||
|
|
||||||
|
if not order.pos_order_id and order.state == 'sale':
|
||||||
|
try:
|
||||||
|
if hasattr(order, '_create_pos_order_for_kds'):
|
||||||
|
order._create_pos_order_for_kds(order)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error("Failed to create POS order during accept: %s", str(e))
|
||||||
|
|
||||||
|
if order.pos_order_id:
|
||||||
|
order.pos_order_id.action_confirm_online_order()
|
||||||
|
else:
|
||||||
|
order.is_online_order_accepted_manually = True
|
||||||
|
|
||||||
|
order._compute_online_order_status()
|
||||||
|
return {'type': 'ir.actions.client', 'tag': 'reload'}
|
||||||
|
|
||||||
|
def action_reject_online_order(self):
|
||||||
|
for order in self:
|
||||||
|
if order.state in ['draft', 'sent', 'sale']:
|
||||||
|
order._action_cancel()
|
||||||
|
|
||||||
|
if order.pos_order_id:
|
||||||
|
order.pos_order_id.action_reject_online_order()
|
||||||
|
|
||||||
|
order._compute_online_order_status()
|
||||||
|
return {'type': 'ir.actions.client', 'tag': 'reload'}
|
||||||
|
|
||||||
|
def _check_cart_is_ready_to_be_paid(self):
|
||||||
|
try:
|
||||||
|
return super(SaleOrderOnline, self)._check_cart_is_ready_to_be_paid()
|
||||||
|
except Exception as e:
|
||||||
|
if self.fulfilment_type == 'pickup' and type(e).__name__ == 'ValidationError':
|
||||||
|
err_str = str(e).lower()
|
||||||
|
if 'shipping method' in err_str or 'delivery' in err_str:
|
||||||
|
return True
|
||||||
|
raise e
|
||||||
|
|
||||||
|
payment_option = fields.Selection([
|
||||||
|
('in_store', 'In Store'),
|
||||||
|
('terminal_in_store', 'Payment Terminal (In Store)'),
|
||||||
|
('terminal_customer', 'Payment Terminal (Customer Place)'),
|
||||||
|
('online_gateway', 'Online Payment Gateway'),
|
||||||
|
('cash', 'Cash'),
|
||||||
|
('interac', 'Interac'),
|
||||||
|
], string='Payment Option', tracking=True)
|
||||||
|
|
||||||
|
# delivery_time = fields.Datetime(string='Requested Delivery Time', tracking=True)
|
||||||
|
|
||||||
|
telephone_number = fields.Char('Telephone Number')
|
||||||
|
|
||||||
|
reservation_source = fields.Selection([
|
||||||
|
('online', 'Online'),
|
||||||
|
('phone', 'Phone'),
|
||||||
|
('staff', 'Staff'),
|
||||||
|
], string='Reservation Source', tracking=True)
|
||||||
|
|
||||||
|
reservation_status = fields.Selection([
|
||||||
|
('draft', 'Request Received'),
|
||||||
|
('confirmed', 'Confirmed'),
|
||||||
|
('arrived', 'Arrived'),
|
||||||
|
('seated', 'Seated'),
|
||||||
|
('cancelled', 'Cancelled'),
|
||||||
|
], string='Reservation Status', default='draft', tracking=True)
|
||||||
|
|
||||||
|
def _is_shippable_order(self):
|
||||||
|
"""
|
||||||
|
Treat pickup, delivery and other types as non-shippable for Odoo's standard validation.
|
||||||
|
This enables 'Billing-only' checkout which is more reliable for payment providers.
|
||||||
|
The Uber delivery line is protected by our _remove_delivery_line override.
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
if self.fulfilment_type in ['pickup', 'delivery', 'dine_in', 'walk_in']:
|
||||||
|
return False
|
||||||
|
return super()._is_shippable_order()
|
||||||
|
|
||||||
|
def _check_carrier_quotation(self, force_carrier_id=None, **kwargs):
|
||||||
|
"""Allow proceeding to payment if we already have a carrier (Uber) or don't need one"""
|
||||||
|
self.ensure_one()
|
||||||
|
_logger.info("Checking carrier quotation for order %s (fulfilment: %s, carrier: %s)", self.name, self.fulfilment_type, self.carrier_id.name if self.carrier_id else 'None')
|
||||||
|
if self.fulfilment_type in ['pickup', 'delivery', 'dine_in', 'walk_in']:
|
||||||
|
return True
|
||||||
|
# If we have a carrier set by our Uber integration, trust it and skip standard re-validation
|
||||||
|
if self.carrier_id and 'Uber' in (self.carrier_id.name or ''):
|
||||||
|
return True
|
||||||
|
return super()._check_carrier_quotation(force_carrier_id=force_carrier_id, **kwargs)
|
||||||
|
|
||||||
|
def _remove_delivery_line(self):
|
||||||
|
"""
|
||||||
|
Prevent Odoo from removing the delivery line if its an Uber order.
|
||||||
|
Odoo often tries to clean up delivery lines on page transitions if it
|
||||||
|
thinks the shipping method is no longer valid.
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
if self.carrier_id and 'Uber' in (self.carrier_id.name or ''):
|
||||||
|
_logger.info("Protecting Uber delivery line from removal on order %s", self.name)
|
||||||
|
return True
|
||||||
|
return super()._remove_delivery_line()
|
||||||
|
|
||||||
|
def _create_pos_order_for_kds(self, sale_order):
|
||||||
|
"""
|
||||||
|
Override from dine360_kds to also mark the POS order as an online order.
|
||||||
|
This method is called by dine360_kds.website_sale_integration when a
|
||||||
|
website sale order is confirmed.
|
||||||
|
"""
|
||||||
|
# Let the parent create the POS order
|
||||||
|
super(SaleOrderOnline, self)._create_pos_order_for_kds(sale_order)
|
||||||
|
|
||||||
|
# Now find the POS order that was just created and mark it
|
||||||
|
# We look for the most recent POS order linked to this sale order's partner
|
||||||
|
# with the note containing the sale order name
|
||||||
|
PosOrder = self.env['pos.order']
|
||||||
|
pos_order = PosOrder.search([
|
||||||
|
('note', 'like', sale_order.name),
|
||||||
|
], order='id desc', limit=1)
|
||||||
|
|
||||||
|
if pos_order:
|
||||||
|
pos_order.write({
|
||||||
|
'is_online_order': True,
|
||||||
|
'online_order_status': 'pending',
|
||||||
|
'sale_order_id': sale_order.id,
|
||||||
|
'online_order_date': fields.Datetime.now(),
|
||||||
|
'order_source': sale_order.order_source or 'online',
|
||||||
|
'fulfilment_type': sale_order.fulfilment_type or 'pickup',
|
||||||
|
# 'delivery_time': sale_order.delivery_time,
|
||||||
|
# 'uber_eta': sale_order.delivery_time,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Link back to sale order
|
||||||
|
sale_order.write({'pos_order_id': pos_order.id})
|
||||||
|
|
||||||
|
# Check if paid via gateway (custom field) or standard Odoo transaction (Stripe, etc.)
|
||||||
|
has_paid_transaction = any(t.state in ['authorized', 'done'] for t in sale_order.transaction_ids)
|
||||||
|
if (sale_order.payment_option == 'online_gateway' or has_paid_transaction) and sale_order.amount_total > 0:
|
||||||
|
payment_method = pos_order._get_online_payment_method()
|
||||||
|
if payment_method:
|
||||||
|
_logger.info("Recording online payment for POS order %s from Sale Order %s", pos_order.name, sale_order.name)
|
||||||
|
payment_data = {
|
||||||
|
'amount': sale_order.amount_total,
|
||||||
|
'payment_date': fields.Datetime.now(),
|
||||||
|
'payment_method_id': payment_method.id,
|
||||||
|
'pos_order_id': pos_order.id,
|
||||||
|
}
|
||||||
|
if hasattr(pos_order, 'add_payment'):
|
||||||
|
pos_order.add_payment(payment_data)
|
||||||
|
else:
|
||||||
|
pos_order.env['pos.payment'].create(payment_data)
|
||||||
|
|
||||||
|
pos_order.env.flush_all()
|
||||||
|
pos_order.invalidate_recordset(['payment_ids', 'amount_paid'])
|
||||||
|
|
||||||
|
# Process as paid so the state changes and payment button disappears safely
|
||||||
|
pos_order.write({'state': 'paid'})
|
||||||
|
try:
|
||||||
|
pos_order._create_order_picking()
|
||||||
|
except AttributeError:
|
||||||
|
_logger.warning("No _create_order_picking method found on POS order")
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error("Error creating picking for online POS order: %s", str(e))
|
||||||
|
|
||||||
|
# Set all lines to a "hold" state - they will go to KDS only when cashier confirms
|
||||||
|
for line in pos_order.lines:
|
||||||
|
if line.product_id.is_kitchen_item:
|
||||||
|
line.write({'preparation_status': 'waiting'})
|
||||||
|
|
||||||
|
# Send bus notification to POS
|
||||||
|
if pos_order.config_id:
|
||||||
|
channel = "online_orders_%s" % pos_order.config_id.id
|
||||||
|
self.env['bus.bus']._sendone(channel, 'new_online_order', {
|
||||||
|
'order_id': pos_order.id,
|
||||||
|
'order_name': pos_order.pos_reference or pos_order.name,
|
||||||
|
'customer_name': sale_order.partner_id.name or 'Guest',
|
||||||
|
'amount_total': pos_order.amount_total,
|
||||||
|
'items_count': len(pos_order.lines),
|
||||||
|
})
|
||||||
|
|
||||||
|
_logger.info(
|
||||||
|
"Marked POS Order %s as online order from Sale Order %s",
|
||||||
|
pos_order.name, sale_order.name
|
||||||
|
)
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_pos_order_online,pos.order.online,point_of_sale.model_pos_order,point_of_sale.group_pos_user,1,1,1,0
|
||||||
|
BIN
addons/dine360_online_orders/static/description/icon.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
320
addons/dine360_online_orders/static/src/css/online_orders.css
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
/* ============================================ */
|
||||||
|
/* Dine360 Online Orders Screen - POS */
|
||||||
|
/* ============================================ */
|
||||||
|
|
||||||
|
.online-orders-screen {
|
||||||
|
background: #171422;
|
||||||
|
color: #eee;
|
||||||
|
font-family: 'Inter', 'Segoe UI', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.online-orders-header {
|
||||||
|
background: #171422;
|
||||||
|
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-orders-title {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-count-badge {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
animation: pulse-badge 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-badge {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back {
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-refresh {
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-refresh:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Body */
|
||||||
|
.online-orders-body {
|
||||||
|
background: #171422;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Left Panel - Orders List */
|
||||||
|
.online-orders-list {
|
||||||
|
width: 380px;
|
||||||
|
min-width: 380px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
background: rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Order Card */
|
||||||
|
.order-card {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card.selected {
|
||||||
|
background: rgba(214, 17, 30, 0.1);
|
||||||
|
border-color: #d6111e;
|
||||||
|
box-shadow: 0 0 15px rgba(214, 17, 30, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-ref {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-total {
|
||||||
|
color: #53cf8a;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-customer {
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-date {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action Buttons in Card */
|
||||||
|
.btn-confirm-order {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: #27ae60;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm-order:hover {
|
||||||
|
background: #2ecc71;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reject-order {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #e74c3c;
|
||||||
|
color: #e74c3c;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reject-order:hover {
|
||||||
|
background: #e74c3c;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Right Panel - Order Detail */
|
||||||
|
.online-order-detail {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 16px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section {
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section:last-of-type {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Customer Avatar */
|
||||||
|
.customer-avatar {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #d6111e, #1a1d23);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Note Box */
|
||||||
|
.note-box {
|
||||||
|
background: rgba(255, 193, 7, 0.1);
|
||||||
|
border: 1px solid rgba(255, 193, 7, 0.2);
|
||||||
|
color: #ffc107;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Order Lines Table */
|
||||||
|
.order-lines-table {
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-lines-table thead th {
|
||||||
|
border-bottom: 2px solid rgba(255, 255, 255, 0.15);
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 10px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-lines-table tbody td {
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 12px 8px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-lines-table tbody tr:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-row td {
|
||||||
|
border-top: 2px solid rgba(255, 255, 255, 0.2) !important;
|
||||||
|
padding-top: 14px;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-total-amount {
|
||||||
|
color: #53cf8a;
|
||||||
|
font-size: 1.3rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kitchen Badge */
|
||||||
|
.kitchen-badge {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kitchen-badge.active {
|
||||||
|
background: rgba(214, 17, 30, 0.2);
|
||||||
|
color: #d6111e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Detail Action Buttons */
|
||||||
|
.btn-confirm-detail {
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
background: linear-gradient(135deg, #27ae60, #2ecc71);
|
||||||
|
border: none;
|
||||||
|
padding: 14px 24px;
|
||||||
|
box-shadow: 0 4px 15px rgba(46, 204, 113, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-confirm-detail:hover {
|
||||||
|
background: linear-gradient(135deg, #2ecc71, #27ae60);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 6px 20px rgba(46, 204, 113, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-reject-detail {
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 14px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State */
|
||||||
|
.empty-state .empty-icon {
|
||||||
|
font-size: 5rem;
|
||||||
|
color: rgba(255, 255, 255, 0.15);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navbar Button */
|
||||||
|
.online-orders-nav-btn {
|
||||||
|
background: rgba(214, 17, 30, 0.15);
|
||||||
|
border: 1px solid rgba(214, 17, 30, 0.3);
|
||||||
|
color: #d6111e;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-orders-nav-btn:hover {
|
||||||
|
background: #d6111e;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #d6111e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar */
|
||||||
|
.online-orders-list::-webkit-scrollbar,
|
||||||
|
.online-order-detail::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-orders-list::-webkit-scrollbar-thumb,
|
||||||
|
.online-order-detail::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-orders-list::-webkit-scrollbar-track,
|
||||||
|
.online-order-detail::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spinner */
|
||||||
|
.spinner-border {
|
||||||
|
color: #d6111e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.online-orders-list {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.online-order-detail {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-card.selected+.online-order-detail {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
64
addons/dine360_online_orders/static/src/css/service_mode.css
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
/* Update Service Mode Selector Styles */
|
||||||
|
#service_mode_selector {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 16px !important;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-option input[type="radio"]:checked+.service-card {
|
||||||
|
border-color: #FECD4F !important;
|
||||||
|
background-color: #FFFDF6 !important;
|
||||||
|
box-shadow: 0 4px 15px rgba(254, 205, 79, 0.25) !important;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-card {
|
||||||
|
border: 2px solid #e9ecef !important;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-card:hover {
|
||||||
|
border-color: #FECD4F;
|
||||||
|
background-color: #FFFDF6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-card i {
|
||||||
|
font-size: 2rem;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-card h6 {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation for the selector if skipped */
|
||||||
|
.shake-animation {
|
||||||
|
animation: shake 0.5s cubic-bezier(.36, .07, .19, .97) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shake {
|
||||||
|
|
||||||
|
10%,
|
||||||
|
90% {
|
||||||
|
transform: translate3d(-1px, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
20%,
|
||||||
|
80% {
|
||||||
|
transform: translate3d(2px, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
30%,
|
||||||
|
50%,
|
||||||
|
70% {
|
||||||
|
transform: translate3d(-4px, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
40%,
|
||||||
|
60% {
|
||||||
|
transform: translate3d(4px, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
/** @odoo-module */
|
||||||
|
|
||||||
|
import { Navbar } from "@point_of_sale/app/navbar/navbar";
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
|
||||||
|
console.log("[OnlineOrders] Patching Navbar...");
|
||||||
|
|
||||||
|
patch(Navbar.prototype, {
|
||||||
|
onClickOnlineOrders() {
|
||||||
|
this.pos.showScreen("OnlineOrdersScreen");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[OnlineOrders] Navbar patched!");
|
||||||
@ -0,0 +1,177 @@
|
|||||||
|
/** @odoo-module */
|
||||||
|
|
||||||
|
import { registry } from "@web/core/registry";
|
||||||
|
import { useService } from "@web/core/utils/hooks";
|
||||||
|
import { Component, useState, onMounted, onWillUnmount } from "@odoo/owl";
|
||||||
|
import { usePos } from "@point_of_sale/app/store/pos_hook";
|
||||||
|
|
||||||
|
console.log("[OnlineOrders] Module loading...");
|
||||||
|
|
||||||
|
export class OnlineOrdersScreen extends Component {
|
||||||
|
static template = "dine360_online_orders.OnlineOrdersScreen";
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
try {
|
||||||
|
console.log("[OnlineOrders] Setup starting...");
|
||||||
|
this.pos = usePos();
|
||||||
|
|
||||||
|
// Direct access to services to avoid 'methods is not iterable' error
|
||||||
|
this.orm = this.env.services.orm;
|
||||||
|
this.notification = this.env.services.pos_notification;
|
||||||
|
this.busService = this.env.services.bus_service;
|
||||||
|
|
||||||
|
this.state = useState({
|
||||||
|
orders: [],
|
||||||
|
loading: true,
|
||||||
|
selectedOrder: null,
|
||||||
|
confirmingId: null,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[OnlineOrders] Services obtained:", {
|
||||||
|
hasOrm: !!this.orm,
|
||||||
|
hasNotif: !!this.notification,
|
||||||
|
hasBus: !!this.busService
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subscribe to bus notifications for real-time updates
|
||||||
|
const channel = `online_orders_${this.pos.config.id}`;
|
||||||
|
console.log("[OnlineOrders] Subscribing to channel:", channel);
|
||||||
|
|
||||||
|
if (this.busService) {
|
||||||
|
this.busService.addChannel(channel);
|
||||||
|
this._notifHandler = this._onNotification.bind(this);
|
||||||
|
this.busService.addEventListener("notification", this._notifHandler);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[OnlineOrders] Setup Error:", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
this.loadOnlineOrders();
|
||||||
|
// Auto-refresh every 30 seconds
|
||||||
|
this._refreshInterval = setInterval(() => {
|
||||||
|
this.loadOnlineOrders();
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
|
||||||
|
onWillUnmount(() => {
|
||||||
|
if (this._refreshInterval) {
|
||||||
|
clearInterval(this._refreshInterval);
|
||||||
|
}
|
||||||
|
if (this.busService && this._notifHandler) {
|
||||||
|
this.busService.removeEventListener("notification", this._notifHandler);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onNotification(event) {
|
||||||
|
const notifications = event.detail || [];
|
||||||
|
for (const notif of notifications) {
|
||||||
|
if (notif.type === "new_online_order") {
|
||||||
|
console.log("[OnlineOrders] New order received!", notif.payload);
|
||||||
|
this.notification.add(
|
||||||
|
`New Online Order from ${notif.payload.customer_name} - ${this.env.utils.formatCurrency(notif.payload.amount_total)}`
|
||||||
|
);
|
||||||
|
this.loadOnlineOrders();
|
||||||
|
}
|
||||||
|
if (notif.type === "online_order_confirmed" || notif.type === "online_order_rejected") {
|
||||||
|
this.loadOnlineOrders();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadOnlineOrders() {
|
||||||
|
try {
|
||||||
|
this.state.loading = true;
|
||||||
|
this.state.error = null;
|
||||||
|
const orders = await this.orm.call(
|
||||||
|
"pos.order",
|
||||||
|
"get_online_orders",
|
||||||
|
[this.pos.config.id]
|
||||||
|
);
|
||||||
|
this.state.orders = orders;
|
||||||
|
this.state.loading = false;
|
||||||
|
console.log("[OnlineOrders] Loaded", orders.length, "orders");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[OnlineOrders] Error loading orders:", error);
|
||||||
|
this.state.loading = false;
|
||||||
|
this.state.error = "Failed to load orders. Please check your connection.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectOrder(order) {
|
||||||
|
this.state.selectedOrder = order;
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirmOrder(orderId) {
|
||||||
|
try {
|
||||||
|
this.state.confirmingId = orderId;
|
||||||
|
this.state.error = null;
|
||||||
|
await this.orm.call(
|
||||||
|
"pos.order",
|
||||||
|
"action_confirm_online_order",
|
||||||
|
[[orderId]]
|
||||||
|
);
|
||||||
|
this.notification.add("Order confirmed and sent to kitchen! 🍳");
|
||||||
|
// Remove from list
|
||||||
|
this.state.orders = this.state.orders.filter(o => o.id !== orderId);
|
||||||
|
if (this.state.selectedOrder && this.state.selectedOrder.id === orderId) {
|
||||||
|
this.state.selectedOrder = null;
|
||||||
|
}
|
||||||
|
this.state.confirmingId = null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[OnlineOrders] Confirm error:", error);
|
||||||
|
this.notification.add("Failed to confirm order");
|
||||||
|
this.state.error = "Failed to confirm order. It might have been modified.";
|
||||||
|
this.state.confirmingId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async rejectOrder(orderId) {
|
||||||
|
try {
|
||||||
|
this.state.error = null;
|
||||||
|
await this.orm.call(
|
||||||
|
"pos.order",
|
||||||
|
"action_reject_online_order",
|
||||||
|
[[orderId]]
|
||||||
|
);
|
||||||
|
this.notification.add("Order has been rejected");
|
||||||
|
this.state.orders = this.state.orders.filter(o => o.id !== orderId);
|
||||||
|
if (this.state.selectedOrder && this.state.selectedOrder.id === orderId) {
|
||||||
|
this.state.selectedOrder = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[OnlineOrders] Reject error:", error);
|
||||||
|
this.notification.add("Failed to reject order");
|
||||||
|
this.state.error = "Failed to reject order.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDate(isoDate) {
|
||||||
|
if (!isoDate) return "";
|
||||||
|
const d = new Date(isoDate);
|
||||||
|
return d.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
formatCurrency(amount) {
|
||||||
|
return this.env.utils.formatCurrency(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
get orderCount() {
|
||||||
|
return this.state.orders.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
back() {
|
||||||
|
if (this.pos.config.module_pos_restaurant && !this.pos.get_order()) {
|
||||||
|
this.pos.showScreen("FloorScreen");
|
||||||
|
} else {
|
||||||
|
this.pos.showScreen("ProductScreen");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the screen
|
||||||
|
registry.category("pos_screens").add("OnlineOrdersScreen", OnlineOrdersScreen);
|
||||||
|
|
||||||
|
console.log("[OnlineOrders] Screen registered!");
|
||||||
62
addons/dine360_online_orders/static/src/js/service_mode.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import publicWidget from "@web/legacy/js/public/public_widget";
|
||||||
|
import { jsonrpc } from "@web/core/network/rpc_service";
|
||||||
|
|
||||||
|
publicWidget.registry.ServiceModeSelector = publicWidget.Widget.extend({
|
||||||
|
selector: '#service_mode_selector',
|
||||||
|
events: {
|
||||||
|
'change input[name="fulfilment_type"]': '_onChangeServiceMode',
|
||||||
|
},
|
||||||
|
|
||||||
|
start: function () {
|
||||||
|
// Init visual selection
|
||||||
|
this.$('input[name="fulfilment_type"]:checked').closest('.service-option').find('.service-card')
|
||||||
|
.css({ 'border-color': '#FECD4F', 'background-color': '#fffdf6', 'box-shadow': '0 4px 10px rgba(254, 205, 79, 0.2)' });
|
||||||
|
return this._super.apply(this, arguments);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onChangeServiceMode: function (ev) {
|
||||||
|
var $input = $(ev.currentTarget);
|
||||||
|
var mode = $input.val();
|
||||||
|
|
||||||
|
// Reset styles
|
||||||
|
this.$('.service-card').css({ 'border-color': '', 'background-color': '', 'box-shadow': '' });
|
||||||
|
// Apply active styles
|
||||||
|
$input.closest('.service-option').find('.service-card')
|
||||||
|
.css({ 'border-color': '#FECD4F', 'background-color': '#fffdf6', 'box-shadow': '0 4px 10px rgba(254, 205, 79, 0.2)' });
|
||||||
|
|
||||||
|
// Hide error if present
|
||||||
|
this.$('#service_mode_error').addClass('d-none');
|
||||||
|
|
||||||
|
// RPC Call to update order
|
||||||
|
jsonrpc('/shop/update_service_mode', {
|
||||||
|
service_mode: mode
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Intercept checkout to ensure a service mode is selected
|
||||||
|
publicWidget.registry.CartCheckoutValidation = publicWidget.Widget.extend({
|
||||||
|
selector: '.oe_cart',
|
||||||
|
events: {
|
||||||
|
'click a[href="/shop/checkout"]': '_onCheckoutClicked',
|
||||||
|
},
|
||||||
|
|
||||||
|
_onCheckoutClicked: function (ev) {
|
||||||
|
// If there's a selector on the page
|
||||||
|
if (this.$('#service_mode_selector').length > 0) {
|
||||||
|
var selectedMode = this.$('input[name="fulfilment_type"]:checked').val();
|
||||||
|
if (!selectedMode) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.$('#service_mode_error').removeClass('d-none');
|
||||||
|
|
||||||
|
// Highlight the box
|
||||||
|
this.$('#service_mode_selector').css('border', '1px solid #dc3545').addClass('shake-animation');
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$('#service_mode_selector').removeClass('shake-animation');
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,261 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<!-- Online Orders Screen -->
|
||||||
|
<t t-name="dine360_online_orders.OnlineOrdersScreen" owl="1">
|
||||||
|
<div class="online-orders-screen screen d-flex flex-column h-100">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="online-orders-header d-flex align-items-center justify-content-between px-4 py-3">
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<button class="btn btn-light btn-back" t-on-click="back">
|
||||||
|
<i class="fa fa-arrow-left me-2"/>Back
|
||||||
|
</button>
|
||||||
|
<h2 class="mb-0 fw-bold online-orders-title">
|
||||||
|
<i class="fa fa-shopping-cart me-2"/>
|
||||||
|
Online Orders
|
||||||
|
<span class="badge bg-danger ms-2 order-count-badge" t-if="orderCount > 0">
|
||||||
|
<t t-esc="orderCount"/>
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline-light btn-refresh" t-on-click="loadOnlineOrders">
|
||||||
|
<i class="fa fa-refresh me-1"/>Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="online-orders-body flex-grow-1 d-flex overflow-hidden position-relative">
|
||||||
|
|
||||||
|
<!-- Error Alert -->
|
||||||
|
<div t-if="state.error" class="position-absolute w-100 p-3" style="z-index: 1050; top: 0;">
|
||||||
|
<div class="alert alert-danger d-flex justify-content-between align-items-center m-0 shadow">
|
||||||
|
<div>
|
||||||
|
<i class="fa fa-exclamation-triangle me-2"/>
|
||||||
|
<span t-esc="state.error"/>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-outline-danger border-0" t-on-click="() => this.state.error = null">
|
||||||
|
<i class="fa fa-times"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div t-if="state.loading" class="d-flex align-items-center justify-content-center w-100">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="spinner-border text-primary mb-3" style="width: 3rem; height: 3rem;"/>
|
||||||
|
<p class="text-muted">Loading online orders...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div t-elif="state.orders.length === 0" class="d-flex align-items-center justify-content-center w-100">
|
||||||
|
<div class="text-center empty-state">
|
||||||
|
<i class="fa fa-inbox empty-icon mb-3"/>
|
||||||
|
<h3 class="text-muted">No Pending Orders</h3>
|
||||||
|
<p class="text-muted">New website orders will appear here automatically</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Orders List -->
|
||||||
|
<t t-else="">
|
||||||
|
<!-- Left Panel: Order Cards -->
|
||||||
|
<div class="online-orders-list p-3">
|
||||||
|
<t t-foreach="state.orders" t-as="order" t-key="order.id">
|
||||||
|
<div t-attf-class="order-card mb-3 p-3 #{state.selectedOrder and state.selectedOrder.id === order.id ? 'selected' : ''}"
|
||||||
|
t-on-click="() => this.selectOrder(order)">
|
||||||
|
<!-- Order Header -->
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
|
<div>
|
||||||
|
<span class="order-ref fw-bold">
|
||||||
|
<i class="fa fa-receipt me-1"/>
|
||||||
|
<t t-esc="order.name"/>
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-info ms-2">ONLINE</span>
|
||||||
|
</div>
|
||||||
|
<span class="order-total fw-bold">
|
||||||
|
<t t-esc="formatCurrency(order.amount_total)"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Customer Info -->
|
||||||
|
<div class="order-customer mb-2">
|
||||||
|
<i class="fa fa-user me-1 text-muted"/>
|
||||||
|
<span t-esc="order.partner_name"/>
|
||||||
|
<t t-if="order.partner_phone">
|
||||||
|
<span class="ms-2 text-muted">
|
||||||
|
<i class="fa fa-phone me-1"/>
|
||||||
|
<t t-esc="order.partner_phone"/>
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Items Summary -->
|
||||||
|
<div class="order-items-summary text-muted small">
|
||||||
|
<t t-esc="order.lines.length"/> items
|
||||||
|
<t t-if="order.sale_order_name">
|
||||||
|
<span class="ms-2">
|
||||||
|
<i class="fa fa-link me-1"/>
|
||||||
|
<t t-esc="order.sale_order_name"/>
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date -->
|
||||||
|
<div class="order-date text-muted small mt-1">
|
||||||
|
<i class="fa fa-clock-o me-1"/>
|
||||||
|
<t t-esc="formatDate(order.date_order)"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div class="order-actions d-flex gap-2 mt-3">
|
||||||
|
<button class="btn btn-success btn-sm flex-grow-1 btn-confirm-order"
|
||||||
|
t-on-click.stop="() => this.confirmOrder(order.id)"
|
||||||
|
t-att-disabled="state.confirmingId === order.id">
|
||||||
|
<t t-if="state.confirmingId === order.id">
|
||||||
|
<span class="spinner-border spinner-border-sm me-1"/>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<i class="fa fa-check me-1"/>
|
||||||
|
</t>
|
||||||
|
Confirm
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger btn-sm flex-grow-1 btn-reject-order"
|
||||||
|
t-on-click.stop="() => this.rejectOrder(order.id)">
|
||||||
|
<i class="fa fa-times me-1"/>Reject
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Panel: Order Detail -->
|
||||||
|
<div class="online-order-detail flex-grow-1 p-3">
|
||||||
|
<t t-if="state.selectedOrder">
|
||||||
|
<div class="detail-card p-4">
|
||||||
|
<h4 class="mb-3 fw-bold">
|
||||||
|
<i class="fa fa-file-text me-2"/>
|
||||||
|
Order Details: <t t-esc="state.selectedOrder.name"/>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<!-- Customer Detail -->
|
||||||
|
<div class="detail-section mb-4">
|
||||||
|
<h6 class="text-muted text-uppercase mb-2">Customer</h6>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<div class="customer-avatar">
|
||||||
|
<i class="fa fa-user"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="fw-bold" t-esc="state.selectedOrder.partner_name"/>
|
||||||
|
<div class="text-muted small" t-if="state.selectedOrder.partner_phone">
|
||||||
|
<i class="fa fa-phone me-1"/>
|
||||||
|
<t t-esc="state.selectedOrder.partner_phone"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Note -->
|
||||||
|
<div class="detail-section mb-4" t-if="state.selectedOrder.note">
|
||||||
|
<h6 class="text-muted text-uppercase mb-2">Note</h6>
|
||||||
|
<div class="note-box p-2 rounded">
|
||||||
|
<t t-esc="state.selectedOrder.note"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Order Lines -->
|
||||||
|
<div class="detail-section mb-4">
|
||||||
|
<h6 class="text-muted text-uppercase mb-2">Items</h6>
|
||||||
|
<table class="table order-lines-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Item</th>
|
||||||
|
<th class="text-center">Qty</th>
|
||||||
|
<th class="text-end">Price</th>
|
||||||
|
<th class="text-end">Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<t t-foreach="state.selectedOrder.lines" t-as="line" t-key="line.id">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span t-attf-class="kitchen-badge #{line.is_kitchen_item ? 'active' : ''}">
|
||||||
|
<i t-attf-class="fa #{line.is_kitchen_item ? 'fa-fire' : 'fa-circle-o'}"/>
|
||||||
|
</span>
|
||||||
|
<span t-esc="line.product_name"/>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted small mt-1" t-if="line.customer_note">
|
||||||
|
<i class="fa fa-comment-o me-1"/>
|
||||||
|
<t t-esc="line.customer_note"/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-center fw-bold">
|
||||||
|
<t t-esc="line.qty"/>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<t t-esc="formatCurrency(line.price_unit)"/>
|
||||||
|
</td>
|
||||||
|
<td class="text-end fw-bold">
|
||||||
|
<t t-esc="formatCurrency(line.price_subtotal_incl)"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</t>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr class="total-row">
|
||||||
|
<td colspan="3" class="text-end fw-bold">Total</td>
|
||||||
|
<td class="text-end fw-bold order-total-amount">
|
||||||
|
<t t-esc="formatCurrency(state.selectedOrder.amount_total)"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Detail Actions -->
|
||||||
|
<div class="detail-actions d-flex gap-3">
|
||||||
|
<button class="btn btn-success btn-lg flex-grow-1 btn-confirm-detail"
|
||||||
|
t-on-click="() => this.confirmOrder(state.selectedOrder.id)"
|
||||||
|
t-att-disabled="state.confirmingId === state.selectedOrder.id">
|
||||||
|
<t t-if="state.confirmingId === state.selectedOrder.id">
|
||||||
|
<span class="spinner-border spinner-border-sm me-2"/>
|
||||||
|
Confirming...
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<i class="fa fa-check-circle me-2"/>
|
||||||
|
Confirm & Send to Kitchen
|
||||||
|
</t>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-danger btn-lg btn-reject-detail"
|
||||||
|
t-on-click="() => this.rejectOrder(state.selectedOrder.id)">
|
||||||
|
<i class="fa fa-times-circle me-1"/>Reject
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<div class="d-flex align-items-center justify-content-center h-100 text-center">
|
||||||
|
<div>
|
||||||
|
<i class="fa fa-hand-pointer-o" style="font-size: 4rem; color: #ccc;"/>
|
||||||
|
<p class="mt-3 text-muted">Select an order to view details</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!-- Navbar button for Online Orders -->
|
||||||
|
<t t-name="dine360_online_orders.NavbarButton" t-inherit="point_of_sale.Navbar" t-inherit-mode="extension" owl="1">
|
||||||
|
<xpath expr="//div[hasclass('status-buttons')]" position="before">
|
||||||
|
<button class="online-orders-nav-btn btn d-flex align-items-center gap-2 h-100 px-3 border-0"
|
||||||
|
t-on-click="onClickOnlineOrders">
|
||||||
|
<i class="fa fa-shopping-cart"/>
|
||||||
|
<span t-if="!ui.isSmall">Online Orders</span>
|
||||||
|
</button>
|
||||||
|
</xpath>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
66
addons/dine360_online_orders/views/backend_online_orders.xml
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<!-- Tree View specifically for Online Orders -->
|
||||||
|
<record id="view_backend_online_order_tree" model="ir.ui.view">
|
||||||
|
<field name="name">sale.order.online.tree</field>
|
||||||
|
<field name="model">sale.order</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree string="Online Orders" decoration-info="online_order_status == 'pending'" decoration-success="online_order_status == 'confirmed'" decoration-muted="online_order_status == 'rejected'" create="false">
|
||||||
|
<field name="name" string="Order Number"/>
|
||||||
|
<field name="date_order" string="Order Date"/>
|
||||||
|
<field name="partner_id" string="Customer"/>
|
||||||
|
<field name="amount_total" string="Total"/>
|
||||||
|
<field name="online_order_status" string="Status" widget="badge" decoration-info="online_order_status == 'pending'" decoration-success="online_order_status == 'confirmed'" decoration-danger="online_order_status == 'rejected'"/>
|
||||||
|
<button name="action_accept_online_order" string="Accept" type="object" class="btn-success" invisible="online_order_status != 'pending'"/>
|
||||||
|
<button name="action_reject_online_order" string="Reject" type="object" class="btn-danger" invisible="online_order_status != 'pending'"/>
|
||||||
|
<field name="state" column_invisible="1"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Form View override for Online Orders buttons -->
|
||||||
|
<record id="view_order_form_online_orders" model="ir.ui.view">
|
||||||
|
<field name="name">sale.order.form.online</field>
|
||||||
|
<field name="model">sale.order</field>
|
||||||
|
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//header" position="inside">
|
||||||
|
<field name="order_source" invisible="1"/>
|
||||||
|
<button name="action_accept_online_order" string="Accept Online Order" type="object" class="btn-success" invisible="order_source != 'online' or online_order_status != 'pending'"/>
|
||||||
|
<button name="action_reject_online_order" string="Reject Online Order" type="object" class="btn-danger" invisible="order_source != 'online' or online_order_status != 'pending'"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='payment_term_id']" position="after">
|
||||||
|
<field name="online_order_status" invisible="order_source != 'online'" readonly="1" widget="badge" decoration-info="online_order_status == 'pending'" decoration-success="online_order_status == 'confirmed'" decoration-danger="online_order_status == 'rejected'"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Action to open Online Orders (Sale Orders from website) -->
|
||||||
|
<record id="action_backend_online_orders" model="ir.actions.act_window">
|
||||||
|
<field name="name">Online Orders</field>
|
||||||
|
<field name="res_model">sale.order</field>
|
||||||
|
<field name="view_mode">tree,kanban,form,calendar,pivot,graph,activity</field>
|
||||||
|
<field name="view_id" ref="view_backend_online_order_tree"/>
|
||||||
|
<field name="domain">[('order_source', '=', 'online')]</field>
|
||||||
|
<field name="context">{'default_order_source': 'online'}</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
No online orders found.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Orders placed on the website will appear here.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Menu Item at the Root Level -->
|
||||||
|
<menuitem id="menu_backend_online_orders_root"
|
||||||
|
name="Online Orders"
|
||||||
|
web_icon="dine360_dashboard,static/src/img/icons/website.svg"
|
||||||
|
sequence="20"
|
||||||
|
action="action_backend_online_orders"/>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
13
addons/dine360_online_orders/views/kds_override_views.xml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Override KDS Dashboard Action to exclude pending online orders -->
|
||||||
|
<record id="dine360_kds.action_kds_dashboard" model="ir.actions.act_window">
|
||||||
|
<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'),
|
||||||
|
'|', ('order_id.is_online_order', '=', False), ('order_id.online_order_status', '!=', 'pending')
|
||||||
|
]</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
22
addons/dine360_online_orders/views/pos_config_views.xml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="res_config_settings_view_form_inherit_dine360" model="ir.ui.view">
|
||||||
|
<field name="name">res.config.settings.view.form.inherit.dine360</field>
|
||||||
|
<field name="model">res.config.settings</field>
|
||||||
|
<field name="priority" eval="95"/>
|
||||||
|
<field name="inherit_id" ref="point_of_sale.res_config_settings_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//block[@id='pos_interface_section']" position="inside">
|
||||||
|
<setting string="Self-Order Kiosk" help="Enable this POS as a self-order kiosk">
|
||||||
|
<field name="is_kiosk"/>
|
||||||
|
<div class="content-group" invisible="not is_kiosk">
|
||||||
|
<div class="mt16">
|
||||||
|
<label string="Default Service Mode" for="kiosk_service_mode" class="col-lg-3 o_light_label"/>
|
||||||
|
<field name="kiosk_service_mode"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
75
addons/dine360_online_orders/views/pos_order_views.xml
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Tree View for Online Orders -->
|
||||||
|
<record id="view_pos_order_online_tree" model="ir.ui.view">
|
||||||
|
<field name="name">pos.order.online.tree</field>
|
||||||
|
<field name="model">pos.order</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree string="Online Orders" decoration-warning="online_order_status=='pending'" decoration-success="online_order_status=='confirmed'" decoration-danger="online_order_status=='rejected'">
|
||||||
|
<field name="pos_reference"/>
|
||||||
|
<field name="partner_id"/>
|
||||||
|
<field name="date_order"/>
|
||||||
|
<field name="amount_total"/>
|
||||||
|
<field name="online_order_status" widget="badge"
|
||||||
|
decoration-warning="online_order_status=='pending'"
|
||||||
|
decoration-success="online_order_status=='confirmed'"
|
||||||
|
decoration-danger="online_order_status=='rejected'"/>
|
||||||
|
<field name="sale_order_id"/>
|
||||||
|
<field name="order_source"/>
|
||||||
|
<field name="fulfilment_type" widget="badge"
|
||||||
|
decoration-info="fulfilment_type=='pickup'"
|
||||||
|
decoration-primary="fulfilment_type=='delivery'"
|
||||||
|
decoration-muted="fulfilment_type=='dine_in'"/>
|
||||||
|
<field name="config_id"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Form View extension -->
|
||||||
|
<record id="view_pos_order_online_form" model="ir.ui.view">
|
||||||
|
<field name="name">pos.order.online.form.inherit</field>
|
||||||
|
<field name="model">pos.order</field>
|
||||||
|
<field name="inherit_id" ref="point_of_sale.view_pos_pos_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='state']" position="after">
|
||||||
|
<field name="is_online_order" invisible="not is_online_order"/>
|
||||||
|
<field name="online_order_status" invisible="not is_online_order" widget="badge"
|
||||||
|
decoration-warning="online_order_status=='pending'"
|
||||||
|
decoration-success="online_order_status=='confirmed'"
|
||||||
|
decoration-danger="online_order_status=='rejected'"/>
|
||||||
|
<field name="sale_order_id" invisible="not is_online_order"/>
|
||||||
|
<field name="order_source" string="Source"/>
|
||||||
|
<field name="fulfilment_type" string="Service"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Action for Online Orders -->
|
||||||
|
<record id="action_online_orders" model="ir.actions.act_window">
|
||||||
|
<field name="name">Online Orders</field>
|
||||||
|
<field name="res_model">pos.order</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
<field name="domain">[('is_online_order', '=', True)]</field>
|
||||||
|
<field name="context">{'search_default_pending': 1}</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Search View -->
|
||||||
|
<record id="view_pos_order_online_search" model="ir.ui.view">
|
||||||
|
<field name="name">pos.order.online.search</field>
|
||||||
|
<field name="model">pos.order</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Online Orders">
|
||||||
|
<field name="pos_reference"/>
|
||||||
|
<field name="partner_id"/>
|
||||||
|
<field name="sale_order_id"/>
|
||||||
|
<filter name="pending" string="Pending" domain="[('online_order_status', '=', 'pending')]"/>
|
||||||
|
<filter name="confirmed" string="Confirmed" domain="[('online_order_status', '=', 'confirmed')]"/>
|
||||||
|
<filter name="rejected" string="Rejected" domain="[('online_order_status', '=', 'rejected')]"/>
|
||||||
|
<group expand="0" string="Group By">
|
||||||
|
<filter name="group_status" string="Status" context="{'group_by': 'online_order_status'}"/>
|
||||||
|
<filter name="group_date" string="Date" context="{'group_by': 'date_order'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Removed cart_service_mode template as requested by user to move it to checkout page -->
|
||||||
|
</odoo>
|
||||||
2
addons/dine360_order_channels/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from . import models
|
||||||
|
from . import controllers
|
||||||
34
addons/dine360_order_channels/__manifest__.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
'name': 'Dine360 Order Channels',
|
||||||
|
'version': '17.0.1.0',
|
||||||
|
'category': 'Sales/Point of Sale',
|
||||||
|
'summary': 'Multi-channel order intake: Phone, WhatsApp, Social, Kiosk, Online',
|
||||||
|
'description': """
|
||||||
|
Extends POS to support multiple order intake channels:
|
||||||
|
- Channel 1: Telephone Orders (order_source=phone, fulfilment, delivery address)
|
||||||
|
- Channel 2: WhatsApp Orders (order_source=whatsapp, number tracking)
|
||||||
|
- Channel 3: Social Media Orders (order_source=social_media, ref tracking)
|
||||||
|
- Channel 4: Kiosk / QR Orders (order_source=kiosk/qr)
|
||||||
|
- Fulfilment Type: Dine-In, Pickup, Delivery
|
||||||
|
- Address capture for Delivery orders with partner search
|
||||||
|
""",
|
||||||
|
'author': 'Dine360',
|
||||||
|
'depends': ['point_of_sale', 'dine360_restaurant'],
|
||||||
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'views/pos_order_views.xml',
|
||||||
|
],
|
||||||
|
'assets': {
|
||||||
|
'point_of_sale._assets_pos': [
|
||||||
|
'dine360_order_channels/static/src/css/channel_panel.css',
|
||||||
|
'dine360_order_channels/static/src/js/order_channel_model.js',
|
||||||
|
'dine360_order_channels/static/src/js/channel_panel.js',
|
||||||
|
'dine360_order_channels/static/src/js/product_screen_patch.js',
|
||||||
|
'dine360_order_channels/static/src/xml/channel_panel.xml',
|
||||||
|
'dine360_order_channels/static/src/xml/receipt_extension.xml',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'installable': True,
|
||||||
|
'application': False,
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
1
addons/dine360_order_channels/controllers/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import main
|
||||||
44
addons/dine360_order_channels/controllers/main.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
from odoo import http
|
||||||
|
from odoo.http import request
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Dine360OrderChannelsController(http.Controller):
|
||||||
|
|
||||||
|
@http.route('/dine360/order_channels/partners', type='json', auth='user', methods=['POST'])
|
||||||
|
def search_partners(self, query='', limit=10):
|
||||||
|
"""Search for partners (for delivery address lookup from POS)"""
|
||||||
|
domain = [('name', 'ilike', query)]
|
||||||
|
partners = request.env['res.partner'].search(domain, limit=limit)
|
||||||
|
return [{
|
||||||
|
'id': p.id,
|
||||||
|
'name': p.name,
|
||||||
|
'phone': p.phone or p.mobile or '',
|
||||||
|
'street': p.street or '',
|
||||||
|
'city': p.city or '',
|
||||||
|
'zip': p.zip or '',
|
||||||
|
'display_name': p.display_name,
|
||||||
|
} for p in partners]
|
||||||
|
|
||||||
|
@http.route('/dine360/order_channels/create_partner', type='json', auth='user', methods=['POST'])
|
||||||
|
def create_partner(self, name, phone='', street='', city='', zip_code=''):
|
||||||
|
"""Quick create a delivery partner from POS"""
|
||||||
|
partner = request.env['res.partner'].create({
|
||||||
|
'name': name,
|
||||||
|
'phone': phone,
|
||||||
|
'street': street,
|
||||||
|
'city': city,
|
||||||
|
'zip': zip_code,
|
||||||
|
'type': 'delivery',
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
'id': partner.id,
|
||||||
|
'name': partner.name,
|
||||||
|
'phone': partner.phone or '',
|
||||||
|
'street': partner.street or '',
|
||||||
|
'city': partner.city or '',
|
||||||
|
'zip': partner.zip or '',
|
||||||
|
}
|
||||||
2
addons/dine360_order_channels/models/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from . import pos_order
|
||||||
|
from . import pos_config
|
||||||
30
addons/dine360_order_channels/models/pos_config.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from odoo import models, fields
|
||||||
|
|
||||||
|
|
||||||
|
class PosConfigChannels(models.Model):
|
||||||
|
_inherit = 'pos.config'
|
||||||
|
|
||||||
|
default_order_source = fields.Selection([
|
||||||
|
('walk_in', 'Walk-In (Standard POS)'),
|
||||||
|
('phone', 'Telephone Order'),
|
||||||
|
('online', 'Online / eCommerce'),
|
||||||
|
('whatsapp', 'WhatsApp'),
|
||||||
|
('social_media', 'Social Media'),
|
||||||
|
('platform', 'Third-Party Platform'),
|
||||||
|
('kiosk', 'Self-Order Kiosk'),
|
||||||
|
('qr', 'QR Table Order'),
|
||||||
|
], string='Default Order Source', default='walk_in',
|
||||||
|
help='Pre-select this order source when opening a new order in this terminal')
|
||||||
|
|
||||||
|
default_fulfilment_type = fields.Selection([
|
||||||
|
('dine_in', 'Dine-In'),
|
||||||
|
('pickup', 'Pickup'),
|
||||||
|
('delivery', 'Delivery'),
|
||||||
|
], string='Default Fulfilment Type', default='dine_in',
|
||||||
|
help='Pre-select this fulfilment type for new orders on this terminal')
|
||||||
|
|
||||||
|
show_channel_panel = fields.Boolean(
|
||||||
|
string='Show Channel / Fulfilment Panel',
|
||||||
|
default=True,
|
||||||
|
help='Show the Order Source and Fulfilment Type selector on the order screen'
|
||||||
|
)
|
||||||
147
addons/dine360_order_channels/models/pos_order.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
from odoo import models, fields, api, _
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PosOrderChannels(models.Model):
|
||||||
|
_inherit = 'pos.order'
|
||||||
|
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
# Channel 1-5: Order Source Field
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
order_source = fields.Selection([
|
||||||
|
('walk_in', 'Walk-In (Standard POS)'),
|
||||||
|
('phone', 'Telephone Order'),
|
||||||
|
('online', 'Online / eCommerce'),
|
||||||
|
('whatsapp', 'WhatsApp'),
|
||||||
|
('social_media', 'Social Media'),
|
||||||
|
('platform', 'Third-Party Platform'),
|
||||||
|
('kiosk', 'Self-Order Kiosk'),
|
||||||
|
('qr', 'QR Table Order'),
|
||||||
|
], string='Order Source', default='walk_in', index=True,
|
||||||
|
help='Channel through which this order was received')
|
||||||
|
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
# All Channels: Fulfilment Type
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
fulfilment_type = fields.Selection([
|
||||||
|
('dine_in', 'Dine-In'),
|
||||||
|
('pickup', 'Pickup'),
|
||||||
|
('delivery', 'Delivery'),
|
||||||
|
], string='Fulfilment Type', default='dine_in', index=True,
|
||||||
|
help='How the customer wants to receive the order')
|
||||||
|
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
# Delivery Address (linked to res.partner)
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
delivery_partner_id = fields.Many2one(
|
||||||
|
'res.partner', string='Delivery Address',
|
||||||
|
domain="[('type', 'in', ['delivery', 'contact', 'other'])]",
|
||||||
|
help='Delivery address for this order'
|
||||||
|
)
|
||||||
|
delivery_street = fields.Char('Delivery Street', compute='_compute_delivery_address', store=True, readonly=False)
|
||||||
|
delivery_city = fields.Char('Delivery City', compute='_compute_delivery_address', store=True, readonly=False)
|
||||||
|
delivery_zip = fields.Char('Delivery Zip', compute='_compute_delivery_address', store=True, readonly=False)
|
||||||
|
delivery_phone = fields.Char('Delivery Phone', compute='_compute_delivery_address', store=True, readonly=False)
|
||||||
|
delivery_notes = fields.Text('Delivery Notes', help='Special delivery instructions')
|
||||||
|
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
# WhatsApp: sender info
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
whatsapp_number = fields.Char('WhatsApp Number', help='Customer WhatsApp phone number')
|
||||||
|
whatsapp_msg_id = fields.Char('WhatsApp Msg ID', help='Original message ID for thread linking')
|
||||||
|
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
# Social Media: reference
|
||||||
|
# ----------------------------------------------------------- # Social Media: reference
|
||||||
|
social_ref = fields.Char('Social Ref', help='Instagram/Facebook message/post reference')
|
||||||
|
|
||||||
|
# Telephone: customer phone for phone orders
|
||||||
|
telephone_number = fields.Char('Telephone Number', help='Customer phone number for telephone orders')
|
||||||
|
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
# Computed helpers
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
is_delivery_order = fields.Boolean(
|
||||||
|
string='Is Delivery', compute='_compute_is_delivery_order', store=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends('fulfilment_type')
|
||||||
|
def _compute_is_delivery_order(self):
|
||||||
|
for order in self:
|
||||||
|
order.is_delivery_order = (order.fulfilment_type == 'delivery')
|
||||||
|
|
||||||
|
@api.depends('delivery_partner_id')
|
||||||
|
def _compute_delivery_address(self):
|
||||||
|
for order in self:
|
||||||
|
p = order.delivery_partner_id
|
||||||
|
if p:
|
||||||
|
order.delivery_street = p.street or ''
|
||||||
|
order.delivery_city = p.city or ''
|
||||||
|
order.delivery_zip = p.zip or ''
|
||||||
|
order.delivery_phone = p.phone or p.mobile or ''
|
||||||
|
else:
|
||||||
|
order.delivery_street = order.delivery_street or ''
|
||||||
|
order.delivery_city = order.delivery_city or ''
|
||||||
|
order.delivery_zip = order.delivery_zip or ''
|
||||||
|
order.delivery_phone = order.delivery_phone or ''
|
||||||
|
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
# Validation
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
@api.constrains('fulfilment_type', 'delivery_street')
|
||||||
|
def _check_delivery_address(self):
|
||||||
|
for order in self:
|
||||||
|
if order.fulfilment_type == 'delivery' and order.state == 'done':
|
||||||
|
if not (order.delivery_street or order.delivery_partner_id):
|
||||||
|
raise ValidationError(
|
||||||
|
_("A delivery address is required for delivery orders.")
|
||||||
|
)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
# Override _order_fields for POS sync (these values come from POS UI)
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
@api.model
|
||||||
|
def _order_fields(self, ui_order):
|
||||||
|
fields_dict = super()._order_fields(ui_order)
|
||||||
|
fields_dict['order_source'] = ui_order.get('order_source', 'walk_in')
|
||||||
|
fields_dict['fulfilment_type'] = ui_order.get('fulfilment_type', 'dine_in')
|
||||||
|
fields_dict['delivery_street'] = ui_order.get('delivery_street', '')
|
||||||
|
fields_dict['delivery_city'] = ui_order.get('delivery_city', '')
|
||||||
|
fields_dict['delivery_zip'] = ui_order.get('delivery_zip', '')
|
||||||
|
fields_dict['delivery_phone'] = ui_order.get('delivery_phone', '')
|
||||||
|
fields_dict['delivery_notes'] = ui_order.get('delivery_notes', '')
|
||||||
|
fields_dict['whatsapp_number'] = ui_order.get('whatsapp_number', '')
|
||||||
|
fields_dict['social_ref'] = ui_order.get('social_ref', '')
|
||||||
|
fields_dict['telephone_number'] = ui_order.get('telephone_number', '')
|
||||||
|
return fields_dict
|
||||||
|
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
# Export fields back to POS (needed for POS to read saved orders)
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
def _export_for_ui(self, order):
|
||||||
|
result = super()._export_for_ui(order)
|
||||||
|
result['order_source'] = order.order_source
|
||||||
|
result['fulfilment_type'] = order.fulfilment_type
|
||||||
|
result['delivery_street'] = order.delivery_street or ''
|
||||||
|
result['delivery_city'] = order.delivery_city or ''
|
||||||
|
result['delivery_zip'] = order.delivery_zip or ''
|
||||||
|
result['delivery_phone'] = order.delivery_phone or ''
|
||||||
|
result['delivery_notes'] = order.delivery_notes or ''
|
||||||
|
result['whatsapp_number'] = order.whatsapp_number or ''
|
||||||
|
result['social_ref'] = order.social_ref or ''
|
||||||
|
result['telephone_number'] = order.telephone_number or ''
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class PosOrderLineChannels(models.Model):
|
||||||
|
_inherit = 'pos.order.line'
|
||||||
|
|
||||||
|
order_source = fields.Selection(
|
||||||
|
related='order_id.order_source', string='Order Source', store=True
|
||||||
|
)
|
||||||
|
fulfilment_type = fields.Selection(
|
||||||
|
related='order_id.fulfilment_type', string='Fulfilment Type', store=True
|
||||||
|
)
|
||||||
@ -0,0 +1 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
.channel-panel {
|
||||||
|
background: #f9fafb;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-btn {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 20px !important;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-btn.active {
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fulfilment-btn {
|
||||||
|
font-size: 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-panel {
|
||||||
|
background-color: #eef3ff !important;
|
||||||
|
border: 1px solid #c7d8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-dropdown {
|
||||||
|
z-index: 999;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-result:hover {
|
||||||
|
background-color: #f0f4ff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-label {
|
||||||
|
color: #666;
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
129
addons/dine360_order_channels/static/src/js/channel_panel.js
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
/** @odoo-module */
|
||||||
|
|
||||||
|
import { Component, useState } from "@odoo/owl";
|
||||||
|
import { usePos } from "@point_of_sale/app/store/pos_hook";
|
||||||
|
import { jsonrpc } from "@web/core/network/rpc_service";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChannelPanel - shown in the POS OrderScreen.
|
||||||
|
* Allows staff to select:
|
||||||
|
* - Order Source (Walk-in / Phone / WhatsApp / Social / Online)
|
||||||
|
* - Fulfilment Type (Dine-In / Pickup / Delivery)
|
||||||
|
* - Delivery address (when Delivery is selected)
|
||||||
|
*/
|
||||||
|
export class ChannelPanel extends Component {
|
||||||
|
static template = "dine360_order_channels.ChannelPanel";
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.pos = usePos();
|
||||||
|
|
||||||
|
this.SOURCE_LABELS = {
|
||||||
|
walk_in: 'Walk-In',
|
||||||
|
phone: 'Phone',
|
||||||
|
whatsapp: 'WhatsApp',
|
||||||
|
social_media: 'Social',
|
||||||
|
online: 'Online',
|
||||||
|
kiosk: 'Kiosk',
|
||||||
|
qr: 'QR Code',
|
||||||
|
platform: 'Platform',
|
||||||
|
};
|
||||||
|
|
||||||
|
this.FULFILMENT_LABELS = {
|
||||||
|
dine_in: 'Dine-In',
|
||||||
|
pickup: 'Pickup',
|
||||||
|
delivery: 'Delivery',
|
||||||
|
};
|
||||||
|
|
||||||
|
this.state = useState({
|
||||||
|
showDelivery: false,
|
||||||
|
showDetails: false, // For manual fields toggle
|
||||||
|
isCollapsed: false, // Panel collapse state
|
||||||
|
searchQuery: '',
|
||||||
|
searchResults: [],
|
||||||
|
searching: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleDetails() {
|
||||||
|
this.state.showDetails = !this.state.showDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentOrder() {
|
||||||
|
return this.pos.get_order();
|
||||||
|
}
|
||||||
|
|
||||||
|
get orderSource() {
|
||||||
|
return this.currentOrder?.order_source || 'walk_in';
|
||||||
|
}
|
||||||
|
|
||||||
|
get fulfilmentType() {
|
||||||
|
return this.currentOrder?.fulfilment_type || 'dine_in';
|
||||||
|
}
|
||||||
|
|
||||||
|
get isDelivery() {
|
||||||
|
return this.fulfilmentType === 'delivery';
|
||||||
|
}
|
||||||
|
|
||||||
|
get showPanel() {
|
||||||
|
return this.pos?.config?.show_channel_panel !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Source Selector ---
|
||||||
|
onSourceChange(source) {
|
||||||
|
const order = this.currentOrder;
|
||||||
|
if (!order) return;
|
||||||
|
order.setOrderSource(source);
|
||||||
|
// Trigger re-render
|
||||||
|
this.state.showDelivery = this.isDelivery;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Fulfilment Selector ---
|
||||||
|
onFulfilmentChange(type) {
|
||||||
|
const order = this.currentOrder;
|
||||||
|
if (!order) return;
|
||||||
|
order.setFulfilmentType(type);
|
||||||
|
this.state.showDelivery = (type === 'delivery');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Partner Address Search ---
|
||||||
|
async onAddressSearch(ev) {
|
||||||
|
const query = ev.target.value;
|
||||||
|
this.state.searchQuery = query;
|
||||||
|
if (query.length < 2) {
|
||||||
|
this.state.searchResults = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.state.searching = true;
|
||||||
|
const results = await jsonrpc('/dine360/order_channels/partners', { query, limit: 8 });
|
||||||
|
this.state.searchResults = results;
|
||||||
|
this.state.searching = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectPartner(partner) {
|
||||||
|
const order = this.currentOrder;
|
||||||
|
if (!order) return;
|
||||||
|
order.setDeliveryAddress({
|
||||||
|
street: partner.street,
|
||||||
|
city: partner.city,
|
||||||
|
zip: partner.zip,
|
||||||
|
phone: partner.phone,
|
||||||
|
});
|
||||||
|
this.state.searchQuery = partner.display_name;
|
||||||
|
this.state.searchResults = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeliveryFieldChange(field, ev) {
|
||||||
|
const order = this.currentOrder;
|
||||||
|
if (!order) return;
|
||||||
|
// Ad-hoc street/city/zip/phone/notes editing
|
||||||
|
const current = {
|
||||||
|
street: order.delivery_street,
|
||||||
|
city: order.delivery_city,
|
||||||
|
zip: order.delivery_zip,
|
||||||
|
phone: order.delivery_phone,
|
||||||
|
notes: order.delivery_notes,
|
||||||
|
};
|
||||||
|
current[field] = ev.target.value;
|
||||||
|
order.setDeliveryAddress(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,107 @@
|
|||||||
|
/** @odoo-module */
|
||||||
|
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
import { Order } from "@point_of_sale/app/store/models";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patch the POS Order model to carry channel data.
|
||||||
|
* These values are sent to the backend via _order_fields which we overrode in Python.
|
||||||
|
*/
|
||||||
|
patch(Order.prototype, {
|
||||||
|
setup(_defaultObj, options) {
|
||||||
|
super.setup(...arguments);
|
||||||
|
// Initialize with POS config defaults
|
||||||
|
const config = this.pos.config;
|
||||||
|
this.order_source = config.default_order_source || 'walk_in';
|
||||||
|
this.fulfilment_type = config.default_fulfilment_type || 'dine_in';
|
||||||
|
this.delivery_street = '';
|
||||||
|
this.delivery_city = '';
|
||||||
|
this.delivery_zip = '';
|
||||||
|
this.delivery_phone = '';
|
||||||
|
this.delivery_notes = '';
|
||||||
|
this.whatsapp_number = '';
|
||||||
|
this.social_ref = '';
|
||||||
|
this.telephone_number = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
export_as_JSON() {
|
||||||
|
const json = super.export_as_JSON(...arguments);
|
||||||
|
json.order_source = this.order_source;
|
||||||
|
json.fulfilment_type = this.fulfilment_type;
|
||||||
|
json.delivery_street = this.delivery_street;
|
||||||
|
json.delivery_city = this.delivery_city;
|
||||||
|
json.delivery_zip = this.delivery_zip;
|
||||||
|
json.delivery_phone = this.delivery_phone;
|
||||||
|
json.delivery_notes = this.delivery_notes;
|
||||||
|
json.whatsapp_number = this.whatsapp_number;
|
||||||
|
json.social_ref = this.social_ref;
|
||||||
|
json.telephone_number = this.telephone_number;
|
||||||
|
return json;
|
||||||
|
},
|
||||||
|
|
||||||
|
init_from_JSON(json) {
|
||||||
|
super.init_from_JSON(...arguments);
|
||||||
|
this.order_source = json.order_source || 'walk_in';
|
||||||
|
this.fulfilment_type = json.fulfilment_type || 'dine_in';
|
||||||
|
this.delivery_street = json.delivery_street || '';
|
||||||
|
this.delivery_city = json.delivery_city || '';
|
||||||
|
this.delivery_zip = json.delivery_zip || '';
|
||||||
|
this.delivery_phone = json.delivery_phone || '';
|
||||||
|
this.delivery_notes = json.delivery_notes || '';
|
||||||
|
this.whatsapp_number = json.whatsapp_number || '';
|
||||||
|
this.social_ref = json.social_ref || '';
|
||||||
|
this.telephone_number = json.telephone_number || '';
|
||||||
|
},
|
||||||
|
|
||||||
|
setOrderSource(source) {
|
||||||
|
this.order_source = source;
|
||||||
|
},
|
||||||
|
|
||||||
|
setFulfilmentType(type) {
|
||||||
|
this.fulfilment_type = type;
|
||||||
|
},
|
||||||
|
|
||||||
|
setDeliveryAddress(fields) {
|
||||||
|
this.delivery_street = fields.street || '';
|
||||||
|
this.delivery_city = fields.city || '';
|
||||||
|
this.delivery_zip = fields.zip || '';
|
||||||
|
this.delivery_phone = fields.phone || '';
|
||||||
|
this.delivery_notes = fields.notes || '';
|
||||||
|
},
|
||||||
|
|
||||||
|
export_for_printing() {
|
||||||
|
const result = super.export_for_printing(...arguments);
|
||||||
|
|
||||||
|
const SOURCE_LABELS = {
|
||||||
|
walk_in: 'Walk-In',
|
||||||
|
phone: 'Phone',
|
||||||
|
whatsapp: 'WhatsApp',
|
||||||
|
social_media: 'Social',
|
||||||
|
online: 'Online',
|
||||||
|
kiosk: 'Kiosk',
|
||||||
|
qr: 'QR Code',
|
||||||
|
platform: 'Platform',
|
||||||
|
};
|
||||||
|
|
||||||
|
const FULFILMENT_LABELS = {
|
||||||
|
dine_in: 'Dine-In',
|
||||||
|
pickup: 'Pickup',
|
||||||
|
delivery: 'Delivery',
|
||||||
|
};
|
||||||
|
|
||||||
|
result.order_source = this.order_source;
|
||||||
|
result.order_source_label = SOURCE_LABELS[this.order_source] || this.order_source;
|
||||||
|
result.fulfilment_type = this.fulfilment_type;
|
||||||
|
result.fulfilment_type_label = FULFILMENT_LABELS[this.fulfilment_type] || this.fulfilment_type;
|
||||||
|
result.delivery_street = this.delivery_street;
|
||||||
|
result.delivery_city = this.delivery_city;
|
||||||
|
result.delivery_zip = this.delivery_zip;
|
||||||
|
result.delivery_phone = this.delivery_phone;
|
||||||
|
result.delivery_notes = this.delivery_notes;
|
||||||
|
result.whatsapp_number = this.whatsapp_number;
|
||||||
|
result.social_ref = this.social_ref;
|
||||||
|
result.telephone_number = this.telephone_number;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
});
|
||||||