Compare commits

...

138 Commits
main ... main

Author SHA1 Message Date
Alaguraj0361
10509df3fa configure container naming in docker-compose, add restaurant receipt template, and implement QZ printer JS wrapper and print ending cutter comment updated 2026-05-06 17:08:13 +05:30
Alaguraj0361
2115f20e95 qz printer error handling and format updated 2026-04-28 10:10:37 +05:30
metatroncubeswdev
1cccf85702 Disable QZ raw printing by default 2026-04-24 21:35:25 -04:00
metatroncubeswdev
0333967c90 Add QZ raw receipt fallback text 2026-04-24 21:26:10 -04:00
metatroncubeswdev
3f1e8bd7e9 Use QZ command format for raw receipts 2026-04-24 21:21:08 -04:00
metatroncubeswdev
43284d9f1a Print QZ receipts as raw ESC POS 2026-04-24 21:13:31 -04:00
metatroncubeswdev
66af3e010a Show QZ printer setting on POS form 2026-04-24 19:55:24 -04:00
metatroncubeswdev
e88010f6af Show QZ printer settings in POS config 2026-04-24 19:46:17 -04:00
Alaguraj0361
384852d839 implement QZ Tray printer integration for POS configuration 2026-04-20 21:35:07 +05:30
Alaguraj0361
eaf04218d9 implement role-based dashboard redirection, menu filtering, and user access control for restaurant staff modules. 2026-04-11 17:04:07 +05:30
Alaguraj0361
bdc602a686 add user role debugging script and update docker-compose container names and backup documentation 2026-04-11 16:04:47 +05:30
Alaguraj0361
5f34bf9419 implement website order to POS integration with cashier confirmation for KDS dispatch and update docker configuration 2026-04-11 12:45:24 +05:30
Alaguraj0361
833d3e3c31 implement Uber delivery integration in sale.order and update docker-compose container names 2026-04-10 20:09:08 +05:30
Alaguraj0361
c33f40a809 implement unified checkout address layout with delivery/pickup selection and custom order total display 2026-04-10 19:51:27 +05:30
Alaguraj0361
c730b5c95d implement Uber delivery fee integration, add online order management for POS, and update Docker configuration. 2026-04-10 18:41:16 +05:30
Alaguraj0361
696e4045a2 add Uber delivery integration to sale order and update docker container naming conventions 2026-04-10 18:34:32 +05:30
Alaguraj0361
f23b06f571 implement online order integration between website sales and POS with automated payment and KDS status synchronization 2026-04-10 18:19:43 +05:30
Alaguraj0361
3bc6298559 implement unified checkout address layout with fulfillment type selection and enhanced form validation 2026-04-10 17:45:59 +05:30
Alaguraj0361
c6738e71d7 implement online order management, Uber delivery integration, and update Docker container configurations 2026-04-10 15:09:19 +05:30
Alaguraj0361
85873ae560 feat: implement online order management system for POS and Sale orders with KDS integration 2026-04-10 14:32:34 +05:30
Alaguraj0361
375101a50f implement Uber delivery fee integration in sale orders and update docker-compose container naming 2026-04-09 22:58:14 +05:30
Alaguraj0361
d95e1235d2 add container names to docker-compose and implement custom checkout address layout with fulfillment selection 2026-04-09 22:51:22 +05:30
Alaguraj0361
4c80bdf027 add Uber integration for POS/Sales orders and customize checkout address UI with delivery/pickup selection 2026-04-09 22:40:59 +05:30
Alaguraj0361
3c6e33cc37 implement Uber delivery fee integration in sale orders and update docker-compose container naming 2026-04-09 20:47:11 +05:30
Alaguraj0361
c256d80677 implement Uber delivery fee integration and update docker-compose container naming 2026-04-09 20:28:53 +05:30
Alaguraj0361
3a91dc3028 implement Uber delivery fee synchronization in sale orders and update docker container naming conventions 2026-04-09 18:07:45 +05:30
Alaguraj0361
2a6f31faf0 implement unified checkout address UI with fulfillment selection and custom validation logic 2026-04-09 17:56:25 +05:30
Alaguraj0361
f3999ef122 implement Uber Direct integration with configuration model, API client, and webhook controller, and update Docker container naming conventions. 2026-04-09 17:34:48 +05:30
Alaguraj0361
511f50f25a implement Uber Direct webhook and quote controllers and update docker-compose service naming 2026-04-09 16:32:02 +05:30
Alaguraj0361
fdd9d4b7f7 add custom product details page layout and update docker-compose service configuration 2026-04-09 10:19:06 +05:30
Alaguraj0361
f20c554391 add Uber delivery webhook and quote controllers and update docker-compose container naming 2026-04-08 22:38:34 +05:30
Alaguraj0361
b89e103ad5 implement Uber Direct webhook and quote controllers and update Docker container naming conventions 2026-04-08 21:47:19 +05:30
Alaguraj0361
db8ab22069 implement Uber webhook and delivery quote controllers and update docker-compose service names 2026-04-08 13:09:45 +05:30
Alaguraj0361
3408b893cb configure Odoo container names and implement custom checkout UI with fulfillment selection and field validation 2026-04-08 12:59:32 +05:30
Alaguraj0361
132727a5bf implement Uber delivery fee integration in sale orders and update docker-compose container naming conventions 2026-04-08 12:44:47 +05:30
Alaguraj0361
ed6e69e191 add order fulfillment type selection and optimize checkout UI, and update docker container naming conventions 2026-04-08 12:27:23 +05:30
Alaguraj0361
467181c4af implement unified checkout address form with delivery/pickup selection and Uber fee display logic 2026-04-08 11:31:38 +05:30
Alaguraj0361
798a7839e5 add container naming to docker-compose and implement custom checkout address UI with delivery/pickup selection 2026-04-08 10:04:26 +05:30
Alaguraj0361
81838e6bc1 implement custom theme styling and animations for Chennora theme 2026-04-07 11:45:00 +05:30
Alaguraj0361
bdb92a60ab implement custom checkout address UI with delivery/pickup selection and Uber fee integration 2026-04-06 22:39:36 +05:30
Alaguraj0361
ae1980ee9d implement unified checkout address layout with pickup/delivery selection and update docker container configurations 2026-04-06 22:36:44 +05:30
Alaguraj0361
0334bb324c implement unified checkout address layout with dynamic delivery/pickup selection and Uber delivery fee integration 2026-04-06 22:33:39 +05:30
Alaguraj0361
d06d252b94 implement unified checkout address layout with dynamic delivery/pickup selection and Uber fee integration 2026-04-06 22:29:05 +05:30
Alaguraj0361
db88add5f3 implement unified checkout address layout with Uber delivery integration and update Docker container naming conventions 2026-04-06 22:25:48 +05:30
Alaguraj0361
3a1f6d0729 add container names to docker-compose and implement unified checkout address UI with delivery/pickup selection 2026-04-06 22:23:10 +05:30
Alaguraj0361
6584bf5a87 implement order fulfillment selection and Uber delivery integration on checkout page and update docker-compose container naming. 2026-04-06 22:19:07 +05:30
Alaguraj0361
3d05b8642f add POS integration fields to sale.order and update docker-compose container names 2026-04-06 22:16:32 +05:30
Alaguraj0361
dd194bd83d implement checkout morphing for delivery/pickup and add order fulfillment fields to sale orders 2026-04-06 22:14:51 +05:30
Alaguraj0361
55abba227d implement checkout fulfillment type selector and integrate Uber delivery fee logic, and update docker-compose service naming. 2026-04-06 22:07:29 +05:30
Alaguraj0361
a280e64b04 implement Uber delivery fee integration and customize checkout UI, and update docker-compose service names 2026-04-06 22:01:57 +05:30
Alaguraj0361
da3c7a2bfa customize checkout UI for Uber integration and update docker-compose service configuration 2026-04-06 21:57:44 +05:30
Alaguraj0361
b4d292ac95 : integrate Uber delivery fee logic and simplify checkout UI, and update docker-compose container naming conventions 2026-04-06 21:53:16 +05:30
Alaguraj0361
264036f0de add Uber delivery fee display and checkout validation logic, and update docker-compose container naming 2026-04-06 21:50:22 +05:30
Alaguraj0361
d349eaf642 implement Uber delivery fee integration with automated checkout validation and container configuration updates 2026-04-06 21:00:28 +05:30
Alaguraj0361
04ca9cda6a implement Uber delivery fee integration on checkout page and update Docker container naming conventions 2026-04-06 20:57:07 +05:30
Alaguraj0361
46ef774508 implement Uber delivery fee integration on checkout page and update docker container naming conventions 2026-04-06 20:52:34 +05:30
Alaguraj0361
d121f6f492 integrate Uber delivery fee calculation and checkout validation, and update Docker container naming conventions 2026-04-06 20:48:55 +05:30
Alaguraj0361
67995fa0c0 add order type toggle and customize address form fields in checkout 2026-04-06 20:42:34 +05:30
Alaguraj0361
67b2501fca implement delivery vs pickup toggle on checkout page with address field customization and Uber integration support 2026-04-06 20:35:33 +05:30
Alaguraj0361
8749ca6f87 add custom checkout address template with delivery/pickup toggle and field optimizations 2026-04-06 20:27:05 +05:30
Alaguraj0361
00ecad960f add delivery/pickup toggle to checkout, rename address fields, and configure Docker container names 2026-04-06 20:23:41 +05:30
Alaguraj0361
2a98fac10d implement custom checkout address form with delivery/pickup toggle and address field validation 2026-04-06 20:03:03 +05:30
Alaguraj0361
0b867b9315 add delivery vs. pickup checkout toggle, customize address form, and containerize Odoo services 2026-04-06 19:59:00 +05:30
Alaguraj0361
ccc8dcd753 implement custom checkout address form with order type selector and delivery/pickup toggle logic 2026-04-06 19:50:00 +05:30
Alaguraj0361
6d67ae9f47 add delivery/pickup toggle to checkout, customize address form, and update docker container configurations 2026-04-06 19:44:31 +05:30
Alaguraj0361
808ade4636 implement online order management for POS and update docker-compose configuration 2026-04-06 18:10:31 +05:30
Alaguraj0361
376f70feb6 add Uber integration module, implement online order management in POS, and customize website checkout address form 2026-04-06 18:03:10 +05:30
Alaguraj0361
2318ea10e8 integrate Uber Direct delivery services into POS orders and add a channel management UI panel. 2026-04-02 22:27:48 +05:30
Alaguraj0361
dae5dc5220 implement table reservation form template and update docker-compose configuration 2026-04-02 16:34:11 +05:30
Alaguraj0361
c256d16032 latest blogs & articles section updated 2026-04-02 16:13:53 +05:30
Alaguraj0361
2e1b4358b1 implement reservation controller for slot availability and submission logic 2026-03-30 19:35:58 +05:30
Alaguraj0361
3429feece9 implement custom theme styling, blog page structure, and controller logic for Dine360 theme 2026-03-30 18:11:13 +05:30
Alaguraj0361
612192c91e implement custom theme styles and update docker configuration 2026-03-30 17:30:17 +05:30
Alaguraj0361
3ce582b17d initialize dine360_theme_chennora module with assets and data, and update docker-compose configuration 2026-03-30 17:25:48 +05:30
Alaguraj0361
32af877129 add TableReservationController to handle reservation form rendering, slot availability, and submission logic 2026-03-30 12:47:33 +05:30
Alaguraj0361
214cd34671 table reservation mail configuration updated 2026-03-30 11:22:59 +05:30
Alaguraj0361
19527c1673 Merge branch 'main' of https://git.metatroncube.in/admin/odoo-chennora-pos 2026-03-30 10:56:26 +05:30
Alaguraj0361
d3cb5fdb7f implement custom theme styling and update docker configuration 2026-03-30 10:54:37 +05:30
Alaguraj0361
2dff231653 implement reservation controller with slot generation and table allocation logic, and update docker-compose configuration 2026-03-30 10:29:01 +05:30
2a8930f360 Update a.txt 2026-03-26 20:28:10 +00:00
root
182e6d4562 Now working from server by mohan 2026-03-26 20:26:01 +00:00
Alaguraj0361
ad8c68b16d Update Docker Compose external volume declaration and mounting syntax. 2026-03-26 22:57:42 +05:30
Alaguraj0361
fcf81e16e5 Configure client2_pgdata and client2_odoo_data volumes as external with explicit names. 2026-03-26 22:52:54 +05:30
Alaguraj0361
c6d194e181 Update Docker volumes to use external named volumes for db and odoo services. 2026-03-26 22:43:42 +05:30
Alaguraj0361
5d235490ef yml file updated 2026-03-26 22:25:05 +05:30
Alaguraj0361
d620559f60 Add blog pages, a contact form, and a delivery option setting to the Chennora theme. 2026-03-26 20:20:49 +05:30
Alaguraj0361
3afa40b65b Merge branch 'main' of https://git.metatroncube.in/admin/odoo-chennora-pos 2026-03-26 16:26:44 +05:30
Alaguraj0361
a93ef83f92 Add Dine360 Chennora theme, implement self-order menu with delivery options, and update Docker compose configurations. 2026-03-26 16:26:08 +05:30
2d3d30f6ad Update a.txt 2026-03-25 18:20:44 +00:00
Alaguraj0361
471b09861c introduce a new premium 'Rush Mode' POS theme and update docker-compose configuration. 2026-03-25 19:40:59 +05:30
Alaguraj0361
7d7d79185f Implement Odoo checkout address customization with pickup/delivery toggle and update Docker Compose for Odoo container configuration. 2026-03-25 17:47:57 +05:30
Alaguraj0361
614a2641f3 Customize checkout address form by removing company/VAT, renaming zip, and adding a delivery/pickup toggle with dynamic address field management. 2026-03-25 13:04:43 +05:30
root
7ba83d0d15 Updated from server 2026-03-24 07:03:42 +00:00
21a1af6c06 Update a.txt 2026-03-24 06:45:59 +00:00
0c8b378555 Update a.txt 2026-03-24 06:44:43 +00:00
72784fed0d Update a.txt 2026-03-24 06:31:48 +00:00
ac913e44ce Add a.txt 2026-03-24 06:29:25 +00:00
Alaguraj0361
1d204abbec Add KDS Kanban, Tree, and Search views for pos.order.line and a corresponding window action. 2026-03-24 11:56:14 +05:30
Alaguraj0361
db4957032f Add Kitchen Display System (KDS) kanban, tree, and search views along with a window action for POS order lines. 2026-03-24 11:02:23 +05:30
Alaguraj0361
f8958c1ebf Implement Kitchen Display System (KDS) dashboard with kanban, tree, and search views for POS order lines. 2026-03-24 10:50:27 +05:30
Alaguraj0361
2498c50960 implement KDS functionality and order line preparation status tracking with real-time notifications. 2026-03-24 10:46:24 +05:30
Alaguraj0361
1bb8334eac Introduce Dine360 restaurant suite with multi-channel order management and Kitchen Display System. 2026-03-24 10:42:17 +05:30
Alaguraj0361
32553b2f4d Implement KDS integration for POS order lines by adding preparation status, timing, and real-time notification mechanisms, and extend POS orders with source and fulfillment types. 2026-03-24 10:34:12 +05:30
Alaguraj0361
eb242cdbee Introduce the Dine360 Restaurant Suite module and integrate Kitchen Display System (KDS) functionality into POS order lines. 2026-03-24 10:24:30 +05:30
Alaguraj0361
defe514b30 dd Dine360 Restaurant Suite module manifest file. 2026-03-24 10:16:01 +05:30
Alaguraj0361
2478823569 Implement pickup/delivery order type selection on checkout, customizing address fields and removing the old service mode template. 2026-03-23 22:21:47 +05:30
Alaguraj0361
1ab45b9ba7 Add new static assets, styling, and page definitions for the dine360_theme_chennora. 2026-03-23 16:22:47 +05:30
Alaguraj0361
be1c9bd35e Introduce comprehensive website sale views and templates, enhancing product visibility, adding product creation forms, and integrating dine360 order channels and online ordering functionalities. 2026-03-21 17:21:16 +05:30
Alaguraj0361
5531400dc3 add a service mode selector for pickup or delivery options to the website cart page. 2026-03-20 16:12:31 +05:30
Alaguraj0361
73ef8df4b7 Add comprehensive styling for the shop page, including the left sidebar filter and product grid layout. 2026-03-20 15:55:53 +05:30
Alaguraj0361
f9fb63f8c9 implement initial custom styling for the Chennora theme, including header, hero, section, and premium page components. 2026-03-20 13:15:20 +05:30
Alaguraj0361
9d916d4ac0 Add initial custom styles for the Chennora theme via new theme.scss and theme_variables.css files. 2026-03-20 12:59:49 +05:30
Alaguraj0361
b0861cae1e Add a service mode selector to the website cart page, allowing users to choose between pickup and delivery options. 2026-03-20 12:21:53 +05:30
Alaguraj0361
b9e5119dfa Add a service mode selector (pickup/delivery) to the website cart page. 2026-03-20 11:40:32 +05:30
Alaguraj0361
405dae06b5 add a service mode selector for pickup and delivery options to the website cart page. 2026-03-20 11:07:52 +05:30
Alaguraj0361
4e2df91c14 Add a service mode selector (pickup/delivery) to the website shopping cart page. 2026-03-18 10:32:59 +05:30
Alaguraj0361
8b248bee27 Add a service mode selector (pickup/delivery) to the website cart page with corresponding styling and module definition. 2026-03-17 21:18:10 +05:30
Alaguraj0361
832fb9f196 Customize checkout address forms by removing company/VAT fields, renaming zip to postal code, and streamlining shipping sections and wizard steps. 2026-03-17 20:43:18 +05:30
Alaguraj0361
7aafe0c6fb Introduce comprehensive order channel management, including online, self-order, and KDS integration, with detailed fulfilment and delivery options for POS orders and receipts. 2026-03-17 20:26:05 +05:30
Alaguraj0361
216c627369 Implement online order management with service mode selection, KDS integration, and dedicated POS order fields. 2026-03-17 14:56:47 +05:30
Alaguraj0361
d8db1f9334 Add a Dashboard button to the Odoo navigation bar. 2026-03-12 10:54:02 +05:30
Alaguraj0361
46249085cd Add dine360 dashboard module assets including icons, CSS, and main controller. 2026-03-12 10:34:26 +05:30
Alaguraj0361
f7c7359b6c customize Odoo checkout address templates to remove company/VAT fields, rename zip to postal code, and streamline shipping details. 2026-03-10 11:51:05 +05:30
Alaguraj0361
8ddde09c63 Streamline checkout address forms by removing company/VAT, renaming zip to postal code, hiding shipping options, and simplifying shipping steps. 2026-03-10 10:33:03 +05:30
Alaguraj0361
c744485423 Customize checkout and address forms by removing company/VAT fields, renaming zip to postal code, simplifying address titles, hiding shipping options, and renaming the shipping wizard step. 2026-03-09 17:23:22 +05:30
Alaguraj0361
7d20d000f3 add Chennora theme module with shop page UI and checkout address view. 2026-03-09 12:03:39 +05:30
Alaguraj0361
5caf51ecf4 Implement new website layout with custom header and SEO, and add login templates. 2026-03-09 09:25:08 +05:30
Alaguraj0361
daa3fbd056 Introduce Dine360 KDS module and add dashboard button to POS navbar, while removing the category synchronization script. 2026-03-06 22:20:45 +05:30
Alaguraj0361
26c9e252cc add script to synchronize POS categories to public categories for products lacking them. 2026-03-06 22:07:58 +05:30
Alaguraj0361
8ea9a66022 Implement Chennora theme, add dashboard navigation buttons to web and POS, and introduce comprehensive shop page styling. 2026-03-06 21:34:46 +05:30
Alaguraj0361
d58a1fd30f Implement online order management with KDS integration and a custom POS navbar. 2026-03-06 18:00:13 +05:30
Alaguraj0361
015f703026 Customize the web layout by setting the page title to "Chennora", updating the favicon, and adding a "Back to Dashboard" button. 2026-03-04 19:53:25 +05:30
Alaguraj0361
c8ed83248b Implement a custom dashboard for logged-in users with role-based menu filtering, low stock alerts, and branded UI elements. 2026-03-04 19:34:08 +05:30
Alaguraj0361
75292e7b88 Introduce a custom dashboard with role-based menu filtering, low stock alerts, and branded web layout including title, favicon, and a dashboard return button. 2026-03-04 17:44:12 +05:30
Alaguraj0361
efa9b1e14a Updated .gitignore to exclude virtual environments, testing artifacts, temporary files, and database dumps. 2026-03-04 17:32:22 +05:30
55e6b70134 merge upstream 2026-03-04 08:29:52 +00:00
b69bf9bc1c Update README.md 2026-03-03 17:35:24 +00:00
db94d57198 Update README.md 2026-03-03 17:34:14 +00:00
253 changed files with 18367 additions and 1157 deletions

View 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.

34
.gitignore vendored
View File

@ -5,6 +5,8 @@ __pycache__/
*.so
.Python
env/
venv/
.venv/
build/
develop-eggs/
dist/
@ -20,6 +22,15 @@ wheels/
*.egg-info/
.installed.cfg
*.egg
.pytest_cache/
.tox/
.nox/
.coverage
htmlcov/
nosetests.xml
coverage.xml
*.cover
*.log
# Odoo artifacts
*.log
@ -27,19 +38,26 @@ odoo.conf
/data/
/filestore/
/sessions/
/logs/
/dump/
# OS artifacts
.DS_Store
Thumbs.db
ehthumbs.db
Desktop.ini
*.tmp
*.bak
*.swp
*.swo
*~
# IDEs
.idea/
.vscode/
*.swp
*.swo
*~
.history/
*.sublime-project
*.sublime-workspace
# Debug/Temp scripts found in project
inspect_*.py
@ -53,10 +71,10 @@ txt.py
update_error.txt
update_log.txt
addons/*.png
blog_posts_*.json
# Local config/environment
.env
.venv
docker-compose.override.yml
# Log / debug dump files
@ -66,7 +84,13 @@ docker-compose.override.yml
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
blog_posts_*.json
test_*.py
temp_*.py

View File

@ -1,4 +1,4 @@
# Dine360 Odoo Addons
# 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.
@ -12,10 +12,10 @@ This repository contains custom Odoo 17 addons for the Dine360 Restaurant Suite.
- 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/` 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
@ -110,4 +110,4 @@ Helper scripts for view diagnostics and homepage issues:
## Notes
- Homepage content is fully overridden in `addons/dine360_theme_chennora/views/pages.xml`.
- If theme changes dont appear, check for COW (customized) views masking the theme.
- If theme changes dont appear, check for COW (customized) views masking the theme.

1
a.txt Normal file
View File

@ -0,0 +1 @@
wsedfgdfffddxr

View File

@ -6,13 +6,17 @@
'summary': 'Installs all Dine360 Restaurant modules',
'author': 'Dine360',
'depends': [
'dine360_dashboard',
'dine360_restaurant',
'dine360_order_channels',
'dine360_dashboard',
'dine360_theme_chennora',
'dine360_kds',
'dine360_reservation',
'dine360_uber',
'dine360_recipe',
'dine360_self_order',
'dine360_online_orders',
'dine360_pos_navbar',
'mail',
'calendar',
'contacts',

View File

@ -18,6 +18,7 @@
'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',

View File

@ -7,6 +7,7 @@ class CustomHome(Home):
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
@ -16,28 +17,52 @@ class ImageHome(Website):
@http.route('/', type='http', auth='public', website=True, sitemap=True)
def index(self, **kwargs):
# -----------------------------------------------------------
# WEBSITE EDITOR FIX
# When Odoo's Website editor loads the site, it opens it in an
# iframe. We must NOT intercept that request with our backend
# dashboard; instead let the real website homepage render so the
# editor can attach to it.
#
# Detection methods (any one is enough):
# 1. Sec-Fetch-Dest == 'iframe' → browser signals iframe load
# 2. enable_editor param present → explicit editor activation
# 3. ?debug= in query string → editor dev mode coming from /web
# SUPER SAFE EDITOR & IFRAME DETECTION
# -----------------------------------------------------------
fetch_dest = request.httprequest.headers.get('Sec-Fetch-Dest', '')
is_iframe = fetch_dest == 'iframe'
is_editor = kwargs.get('enable_editor') or request.params.get('enable_editor')
if is_iframe or is_editor:
# Let the standard Website controller render the homepage
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)
# Not logged in → show the public website homepage
if not request.session.uid:
return request.render('website.homepage')
# 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([
@ -70,6 +95,37 @@ class ImageHome(Website):
continue
seen_names.add(menu.name)
# 4. Dynamic Icon Override (Dine360 Branding)
# This maps menu names to our custom SVG icons dynamically
icon_mapping = {
'Discuss': 'dine360_dashboard,static/src/img/icons/discuss.svg',
'Calendar': 'dine360_dashboard,static/src/img/icons/calendar.svg',
'Contacts': 'dine360_dashboard,static/src/img/icons/contacts.svg',
'CRM': 'dine360_dashboard,static/src/img/icons/crm.svg',
'Sales': 'dine360_dashboard,static/src/img/icons/sales.svg',
'Dashboards': 'dine360_dashboard,static/src/img/icons/dashboards.svg',
'Point of Sale': 'dine360_dashboard,static/src/img/icons/point_of_sale.svg',
'Invoicing': 'dine360_dashboard,static/src/img/icons/invoicing.svg',
'Website': 'dine360_dashboard,static/src/img/icons/website.svg',
'Purchase': 'dine360_dashboard,static/src/img/icons/purchase.svg',
'Inventory': 'dine360_dashboard,static/src/img/icons/inventory.svg',
'Employees': 'dine360_dashboard,static/src/img/icons/employees.svg',
'Apps': 'dine360_dashboard,static/src/img/icons/apps.svg',
'Settings': 'dine360_dashboard,static/src/img/icons/settings.svg',
'Kitchen (KDS)': 'dine360_dashboard,static/src/img/icons/kitchen_kds.svg',
'Table Reservation': 'dine360_dashboard,static/src/img/icons/table_reservation.svg',
'Uber Integration': 'dine360_dashboard,static/src/img/icons/uber_integration.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():
# We use a virtual field assignment so it doesn't try to save to DB
# but the template picks it up
menu.web_icon = icon_path
break
filtered_menus.append(menu)
# Low Stock Alerts (Ingredients)

View File

@ -71,7 +71,7 @@ body.o_home_dashboard,
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 0) 50%);
background: linear-gradient(135deg, rgb(175 36 36 / 62%) 0%, rgb(181 84 84 / 20%) 50%);
z-index: 1;
}

View File

@ -4,7 +4,7 @@
height: 100vh !important;
width: 100vw !important;
overflow: hidden;
background: url('/dine360_theme_chennora/static/src/img/chen-banner-2.webp') !important;
background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)), url('/dine360_theme_chennora/static/src/img/chen-banner-2.webp') !important;
background-repeat: no-repeat !important;
background-position: center center !important;
background-size: cover !important;
@ -111,8 +111,8 @@
}
.form-control {
background: rgba(255, 255, 255, 0.05) !important;
border: 1px solid rgba(255, 255, 255, 0.1) !important;
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;
@ -120,7 +120,14 @@
}
.form-control::placeholder {
color: rgba(255, 255, 255, 0.3) !important;
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 */
@ -157,7 +164,16 @@
.o_login_right_side {
background: #0f172a !important;
/* Keep dark background on mobile */
}
.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;
}
}

View File

@ -34,9 +34,8 @@
/* height: calc(100vh - 85px) !important;
} */
.pos .leftpane,
.pos .order-container {
/* The Cart Section - Move to Right */
.pos .leftpane {
/* The Cart Section - Now on Right */
width: 480px !important;
border-left: 2px solid #f1f5f9 !important;
border-right: none !important;
@ -44,18 +43,38 @@
display: flex !important;
flex-direction: column !important;
flex: none !important;
height: 37.9vh !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 .rightpane,
.pos .product-list-container {
/* The Product Section - Move to Left */
flex: 1 !important;
flex: 1 1 auto !important;
overflow-y: auto !important;
background: #f8fafc !important;
}
@ -87,17 +106,7 @@
object-fit: contain !important;
}
/* "Rush Mode" Label next to logo */
.pos .pos-logo::after {
content: "RUSH MODE";
font-size: 14px;
font-weight: 800;
color: #ef4444;
letter-spacing: 2px;
border-left: 2px solid #e5e7eb;
padding-left: 20px;
margin-left: 10px;
}
/* "Rush Mode" Label removed for production */
/* 3. Search Bar - Teal Theme */
.pos .search-bar {
@ -299,4 +308,139 @@
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;
}
}

View File

@ -113,7 +113,7 @@
.oe_website_sale #products_grid_before li:hover,
.oe_website_sale #products_grid_before .list-group-item:hover {
background: #2bb1a5 !important;
background: #fecd4f !important;
transform: translateX(5px);
}

View File

@ -97,7 +97,7 @@ body,
.o_main_navbar .o_menu_sections .o_nav_entry,
.o_main_navbar .o_menu_systray .o_nav_entry {
color: white !important;
color: #111111 !important;
font-size: 14px !important;
font-weight: 600 !important;
}

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

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

View File

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

View 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

View 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

View 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

View 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"/>
<!-- 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

View 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"/>
<!-- 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

View 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

View 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>

View File

@ -19,14 +19,6 @@
<i class="fa fa-globe"/>
</a>
<a href="#" class="o_top_item" title="AI Assistant">
<span class="o_ai_icon">AI</span>
</a>
<a href="#" class="o_top_item" title="Search">
<i class="fa fa-search"/>
</a>
<a href="/web#action=mail.action_discuss" class="o_top_item" title="Messages">
<i class="fa fa-comments-o"/>
<span class="badge_dot"/>

View File

@ -37,7 +37,7 @@
</div>
<div class="o_login_footer_custom">
<p>Powered by <a href="https://dine360.com/" target="_blank">Dine360</a></p>
<p>Powered by <a href="https://dine360.com/" target="_blank">Dine360 Inc</a></p>
</div>
</div>
</xpath>

View File

@ -8,9 +8,10 @@
</xpath>
<xpath expr="//body" position="inside">
<t t-if="not request.env.user._is_public()">
<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: 99999;
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;

View File

@ -12,7 +12,7 @@
- Floor/Table based organization
""",
'author': 'Dine360',
'depends': ['point_of_sale', 'pos_restaurant', 'dine360_restaurant', 'sale_management', 'website_sale'],
'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',
@ -24,7 +24,7 @@
'dine360_kds/static/src/css/kds_style.css',
'dine360_kds/static/src/js/kds_backend.js',
],
'point_of_sale.assets_prod': [
'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

View File

@ -25,6 +25,21 @@ class PosOrderLine(models.Model):
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):
@ -82,10 +97,12 @@ class PosOrderLine(models.Model):
def create(self, vals_list):
"""Override create to send notifications to KDS when new orders are added"""
lines = super(PosOrderLine, self).create(vals_list)
# 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()
# 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):
@ -138,6 +155,23 @@ class PosOrderLine(models.Model):
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)

View File

@ -5,6 +5,6 @@ class ProductTemplate(models.Model):
is_kitchen_item = fields.Boolean(
string='Show in KDS',
default=True,
default=False,
help="If checked, this product will appear in the Kitchen Display System when ordered."
)

View File

@ -56,8 +56,9 @@ class SaleOrder(models.Model):
if qty <= 0:
continue
# Skip non-kitchen items (delivery charges, shipping, etc.)
if not line.product_id.is_kitchen_item:
# 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, {
@ -68,19 +69,24 @@ class SaleOrder(models.Model):
'price_subtotal_incl': line.price_total,
'full_product_name': line.name,
'tax_ids': [(6, 0, line.tax_id.ids)],
# Key for KDS:
'preparation_status': 'waiting',
# 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 using sequence
pos_reference = session.config_id.sequence_id.next_by_id() if session.config_id.sequence_id else f"Order {sale_order.name}"
# 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)
pos_order = PosOrder.create({
# 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,
@ -95,5 +101,5 @@ class SaleOrder(models.Model):
# 'state': 'draft', # Default is draft
})
# Trigger KDS notification (handled by create method of pos.order.line in dine360_kds)
_logger.info(f"Created POS Order {pos_order.name} from Website Order {sale_order.name} for KDS.")
# 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).")

View File

@ -2,3 +2,5 @@ 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

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 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
3 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
4 access_kds_order_line_user pos.order.line.user point_of_sale.model_pos_order_line base.group_user 1 1 1 0
5 access_kds_pos_session_kitchen pos.session.kitchen point_of_sale.model_pos_session dine360_restaurant.group_restaurant_kitchen 1 0 0 0
6 access_kds_pos_category_kitchen pos.category.kitchen point_of_sale.model_pos_category dine360_restaurant.group_restaurant_kitchen 1 0 0 0

View File

@ -4,13 +4,14 @@ 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 useService potential conflicts
// 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;
@ -28,8 +29,6 @@ export class KdsKanbanController extends KanbanController {
this.busService.removeEventListener("notification", handler);
}
});
} else {
console.error("[KDS Controller] Bus service not found!");
}
}

View File

@ -12,11 +12,13 @@
<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 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">
@ -25,23 +27,31 @@
</strong>
</div>
<div class="ms-auto h5 mb-0">
<span class="badge rounded-pill bg-light text-dark border">
<i class="fa fa-cutlery me-1"/> <field name="table_id"/>
<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" style="background: rgba(254, 205, 79, 0.1); border-radius: 8px;">
<i class="fa fa-sticky-note-o me-2"/> <strong>Note:</strong> <field name="customer_note"/>
<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">
<span><i class="fa fa-clock-o me-1"/> <field name="create_date"/></span>
<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">
@ -81,6 +91,8 @@
<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"/>

View File

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

View File

@ -0,0 +1,37 @@
{
'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',
],
'assets': {
'point_of_sale._assets_pos': [
'dine360_online_orders/static/src/css/online_orders.css',
'dine360_online_orders/static/src/js/online_orders_screen.js',
'dine360_online_orders/static/src/js/online_orders_navbar.js',
'dine360_online_orders/static/src/xml/online_orders_screen.xml',
],
'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',
}

View File

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

View File

@ -0,0 +1,14 @@
from odoo import http
from odoo.http import request
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

View 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

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

View 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'),
('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

View 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

View File

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

View File

@ -0,0 +1,176 @@
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)
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
)

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_pos_order_online pos.order.online point_of_sale.model_pos_order point_of_sale.group_pos_user 1 1 1 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View 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;
}
}

View 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);
}
}

View File

@ -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!");

View File

@ -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!");

View 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);
}
}
}
});

View File

@ -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 &amp; 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>

View 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>

View 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>

View 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>

View File

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

View File

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

View 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',
}

View File

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

View 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 '',
}

View File

@ -0,0 +1,2 @@
from . import pos_order
from . import pos_config

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

View 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
)

View File

@ -0,0 +1 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink

View File

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

View File

@ -0,0 +1,131 @@
/** @odoo-module */
import { Component, useState } from "@odoo/owl";
import { usePos } from "@point_of_sale/app/store/pos_hook";
import { useService } from "@web/core/utils/hooks";
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.dialog = useService("dialog");
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;
}
// --- 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);
}
}

View File

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

View File

@ -0,0 +1,23 @@
/** @odoo-module */
import { patch } from "@web/core/utils/patch";
import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen";
import { ChannelPanel } from "./channel_panel";
/**
* Patch ProductScreen to:
* 1. Register ChannelPanel as a component
* 2. Expose showChannelPanel computed property to the template
*/
patch(ProductScreen, {
components: {
...ProductScreen.components,
ChannelPanel,
},
});
patch(ProductScreen.prototype, {
get showChannelPanel() {
return this.pos?.config?.show_channel_panel !== false;
},
});

View File

@ -0,0 +1,151 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<!-- ChannelPanel Component -->
<t t-name="dine360_order_channels.ChannelPanel" owl="1">
<t t-if="showPanel and currentOrder">
<div class="channel-panel p-2 border-bottom">
<!-- Header with Collapse Toggle -->
<div class="d-flex justify-content-between align-items-center mb-1" style="cursor: pointer;" t-on-click="() => this.state.isCollapsed = !this.state.isCollapsed">
<span class="fw-bold small text-muted"><i class="fa fa-cogs me-1"/> Order Channels</span>
<button class="btn btn-sm btn-link p-0 text-decoration-none shadow-none text-muted">
<i t-attf-class="fa #{this.state.isCollapsed ? 'fa-chevron-down' : 'fa-chevron-up'}"/>
</button>
</div>
<div t-if="!this.state.isCollapsed" class="d-flex flex-column gap-2 mt-2">
<!-- Order Source Row -->
<div class="channel-section">
<div class="channel-label mb-1"><i class="fa fa-list-alt me-1"></i> ORDER SOURCE</div>
<div class="d-flex flex-wrap gap-1">
<t t-foreach="Object.entries(SOURCE_LABELS)" t-as="entry" t-key="entry[0]">
<button
t-attf-class="btn btn-sm channel-btn #{orderSource === entry[0] ? 'btn-dark active' : 'btn-outline-secondary'}"
t-on-click="() => this.onSourceChange(entry[0])">
<t t-esc="entry[1]"/>
</button>
</t>
</div>
</div>
<!-- Phone number field -->
<div t-if="orderSource === 'phone'" class="channel-extra">
<input type="text" class="form-control form-control-sm"
placeholder="Phone Number"
t-att-value="currentOrder.telephone_number"
t-on-change="(ev) => { currentOrder.telephone_number = ev.target.value; }"/>
</div>
<!-- WhatsApp number field -->
<div t-if="orderSource === 'whatsapp'" class="channel-extra">
<input type="text" class="form-control form-control-sm"
placeholder="WhatsApp Number"
t-att-value="currentOrder.whatsapp_number"
t-on-change="(ev) => { currentOrder.whatsapp_number = ev.target.value; }"/>
</div>
<!-- Social media ref field -->
<div t-if="orderSource === 'social_media'" class="channel-extra">
<input type="text" class="form-control form-control-sm"
placeholder="Post / Message Reference"
t-att-value="currentOrder.social_ref"
t-on-change="(ev) => { currentOrder.social_ref = ev.target.value; }"/>
</div>
<!-- Fulfilment Row -->
<div class="channel-section mt-1">
<div class="channel-label mb-1"><i class="fa fa-rocket me-1"></i> FULFILMENT TYPE</div>
<div class="btn-group w-100" role="group">
<t t-foreach="Object.entries(FULFILMENT_LABELS)" t-as="entry" t-key="entry[0]">
<button
t-attf-class="btn btn-sm fulfilment-btn #{fulfilmentType === entry[0] ? 'btn-primary active' : 'btn-outline-primary'}"
t-on-click="() => this.onFulfilmentChange(entry[0])">
<t t-esc="entry[1]"/>
</button>
</t>
</div>
</div>
<!-- Delivery Address Section -->
<div t-if="isDelivery" class="delivery-panel p-2 rounded mt-1 shadow-sm">
<div class="d-flex justify-content-between align-items-center mb-1">
<div class="fw-bold small text-muted"><i class="fa fa-map-marker text-danger"/> Delivery Address</div>
<button class="btn btn-sm btn-link text-primary p-0 text-decoration-none shadow-none fw-bold"
style="font-size: 10px"
t-on-click="toggleDetails">
<t t-if="state.showDetails"><i class="fa fa-caret-up"/> HIDE FIELDS</t>
<t t-else=""><i class="fa fa-caret-down"/> EDIT MANUAL</t>
</button>
</div>
<!-- Partner search (always visible under Delivery) -->
<div class="position-relative mb-2">
<input type="text" class="form-control form-control-sm border-primary"
placeholder="🔍 Search saved address..."
t-att-value="state.searchQuery"
t-on-input="onAddressSearch"/>
<div t-if="state.searching" class="text-muted small ps-1">Searching...</div>
<div t-if="state.searchResults.length > 0"
class="address-dropdown position-absolute bg-white border rounded shadow w-100 mt-1">
<t t-foreach="state.searchResults" t-as="partner" t-key="partner.id">
<div class="address-result p-2 border-bottom small list-group-item-action"
t-on-click="() => this.onSelectPartner(partner)">
<span class="fw-bold" t-esc="partner.name"/>
<span class="text-muted">
<t t-esc="(partner.street || '') + ', ' + (partner.city || '')"/>
</span>
</div>
</t>
</div>
</div>
<!-- Manual fields (hidden by default) -->
<div t-if="state.showDetails" class="manual-address-fields mt-2 p-2 border-top bg-white rounded">
<input type="text" class="form-control form-control-sm mb-1"
placeholder="Street"
t-att-value="currentOrder.delivery_street"
t-on-change="(ev) => this.onDeliveryFieldChange('street', ev)"/>
<div class="d-flex gap-1 mb-1">
<input type="text" class="form-control form-control-sm"
placeholder="City"
t-att-value="currentOrder.delivery_city"
t-on-change="(ev) => this.onDeliveryFieldChange('city', ev)"/>
<input type="text" class="form-control form-control-sm"
placeholder="Zip" style="max-width:90px"
t-att-value="currentOrder.delivery_zip"
t-on-change="(ev) => this.onDeliveryFieldChange('zip', ev)"/>
</div>
<input type="tel" class="form-control form-control-sm mb-1"
placeholder="Phone"
t-att-value="currentOrder.delivery_phone"
t-on-change="(ev) => this.onDeliveryFieldChange('phone', ev)"/>
<textarea class="form-control form-control-sm" rows="2"
placeholder="Delivery notes (gate code, floor...)"
t-att-value="currentOrder.delivery_notes"
t-on-change="(ev) => this.onDeliveryFieldChange('notes', ev)"/>
</div>
<!-- Quick Info when hidden -->
<div t-if="!state.showDetails &amp;&amp; currentOrder.delivery_street" class="small mt-1 text-muted fst-italic">
<i class="fa fa-info-circle"/>
<t t-esc="currentOrder.delivery_street"/>,
<t t-esc="currentOrder.delivery_city"/>
</div>
</div>
</div>
</div>
</t>
</t>
<!-- Inject ChannelPanel into the left pane of ProductScreen, above OrderWidget -->
<t t-name="point_of_sale.ProductScreen"
t-inherit="point_of_sale.ProductScreen"
t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('leftpane')]//OrderWidget" position="before">
<ChannelPanel/>
</xpath>
</t>
</templates>

View File

@ -0,0 +1,271 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="dine360_order_channels.OrderReceipt" t-inherit="point_of_sale.OrderReceipt" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('pos-receipt')]" position="replace">
<div class="pos-receipt custom-restaurant-receipt">
<style>
.custom-restaurant-receipt {
width: 100%;
font-family: 'Arial', sans-serif;
color: #333;
background: #fff;
padding: 10px 5px;
}
.custom-restaurant-receipt .main-title {
color: #E67E22;
text-align: center;
font-size: 22px;
font-weight: bold;
margin: 0 0 15px 0;
}
.custom-restaurant-receipt .receipt-header {
text-align: center;
margin-bottom: 10px;
}
.custom-restaurant-receipt .receipt-header img {
max-width: 120px;
margin-bottom: 10px;
}
.custom-restaurant-receipt .company-name {
font-weight: bold;
font-size: 16px;
margin-bottom: 4px;
}
.custom-restaurant-receipt .company-details {
font-size: 12px;
line-height: 1.4;
}
.custom-restaurant-receipt .subtitle {
font-style: italic;
font-weight: bold;
text-align: center;
margin: 15px 0;
font-size: 14px;
}
.custom-restaurant-receipt .info-row {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
font-size: 12px;
}
.custom-restaurant-receipt .info-box {
background: #FDEBD0;
padding: 6px 10px;
border-radius: 2px;
flex: 1;
margin: 0 4px;
text-align: center;
font-weight: bold;
}
.custom-restaurant-receipt .info-box span {
font-weight: normal;
display: block;
margin-top: 2px;
}
.custom-restaurant-receipt .info-box:first-child { margin-left: 0; }
.custom-restaurant-receipt .info-box:last-child { margin-right: 0; }
.custom-restaurant-receipt table {
width: 100%;
border-collapse: collapse;
margin-bottom: 15px;
font-size: 12px;
}
.custom-restaurant-receipt th {
background: #E67E22;
color: white;
padding: 8px 6px;
text-align: right;
}
.custom-restaurant-receipt th:first-child { text-align: left; }
.custom-restaurant-receipt td {
padding: 8px 6px;
text-align: right;
border-bottom: 1px solid #eee;
}
.custom-restaurant-receipt td:first-child { text-align: left; }
.custom-restaurant-receipt .totals-section {
width: 70%;
float: right;
margin-bottom: 15px;
font-size: 13px;
}
.custom-restaurant-receipt .totals-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
}
.custom-restaurant-receipt .totals-row.bold {
font-weight: bold;
}
.custom-restaurant-receipt .totals-row .val-box {
background: #FDEBD0;
padding: 4px 10px;
min-width: 80px;
text-align: right;
border-radius: 2px;
}
.custom-restaurant-receipt .clearfix::after {
content: "";
clear: both;
display: table;
}
.custom-restaurant-receipt .footer-slogan {
text-align: center;
font-weight: bold;
font-style: italic;
font-size: 16px;
margin-top: 20px;
clear: both;
}
.custom-restaurant-receipt .channel-info {
font-size: 11px;
margin-top: 15px;
border-top: 1px dashed #ccc;
padding-top: 10px;
clear: both;
}
</style>
<div class="main-title">Restaurant Receipt</div>
<div class="receipt-header">
<img t-attf-src="/web/image?model=res.company&amp;id={{props.data.headerData.company.id}}&amp;field=logo" alt="Logo"/>
<div class="company-name" t-esc="props.data.headerData.company.name"/>
<div class="company-details">
<div t-if="props.data.headerData.company.contact_address" t-esc="props.data.headerData.company.contact_address"/>
<div>
<t t-if="props.data.headerData.company.phone">Tel: <t t-esc="props.data.headerData.company.phone"/> </t>
<t t-if="props.data.headerData.company.email"> | <t t-esc="props.data.headerData.company.email"/></t>
</div>
<div t-if="props.data.headerData.company.website" t-esc="props.data.headerData.company.website"/>
</div>
</div>
<div class="subtitle">"Authentic Indian Food At its Finest!"</div>
<div class="info-row">
<div class="info-box">
Receipt No.
<span t-esc="props.data.name"/>
</div>
<div class="info-box">
Date &amp; Time
<span t-esc="props.data.date"/>
</div>
<div class="info-box" t-if="props.data.headerData.cashier">
Cashier
<span t-esc="props.data.headerData.cashier"/>
</div>
</div>
<table>
<thead>
<tr>
<th>List of Items</th>
<th>Qty</th>
<th>Unit Cost</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<tr t-foreach="props.data.orderlines" t-as="line" t-key="line.id">
<td>
<t t-esc="line.productName"/>
<div t-if="line.customerNote" class="small italic text-muted mt-1">
<i class="fa fa-sticky-note me-1"/> <t t-esc="line.customerNote"/>
</div>
</td>
<td><t t-esc="line.qty"/></td>
<td><t t-esc="props.formatCurrency(line.price)"/></td>
<td><t t-esc="props.formatCurrency(line.price_display)"/></td>
</tr>
</tbody>
</table>
<div class="clearfix">
<div class="totals-section">
<div class="totals-row">
<span>Total Amount</span>
<span class="val-box" t-esc="props.formatCurrency(props.data.total_without_tax)"/>
</div>
<div class="totals-row" t-if="props.data.amount_tax > 0">
<span>VAT</span>
<span class="val-box" t-esc="props.formatCurrency(props.data.amount_tax)"/>
</div>
<div class="totals-row bold">
<span>Net Amount</span>
<span class="val-box" t-esc="props.formatCurrency(props.data.amount_total)"/>
</div>
<t t-if="props.data.rounding_applied">
<div class="totals-row">
<span>Rounding</span>
<span class="val-box" t-esc="props.formatCurrency(props.data.rounding_applied)"/>
</div>
<div class="totals-row bold">
<span>To Pay</span>
<span class="val-box" t-esc="props.formatCurrency(props.data.amount_total + props.data.rounding_applied)"/>
</div>
</t>
<!-- Payment Lines -->
<div class="totals-row" t-foreach="props.data.paymentlines" t-as="line" t-key="line_index">
<span t-esc="line.name"/>
<span class="val-box" t-esc="props.formatCurrency(line.amount, false)"/>
</div>
<div class="totals-row" t-if="props.data.change > 0">
<span>Change</span>
<span class="val-box" t-esc="props.formatCurrency(props.data.change)"/>
</div>
</div>
</div>
<div class="footer-slogan">Eat As much As You Like!</div>
<!-- Order Channels Info -->
<div class="channel-info">
<div t-if="props.data.order_source" class="d-flex justify-content-between mb-1">
<span>Order Source:</span>
<span class="fw-bold text-uppercase" t-esc="props.data.order_source_label"/>
</div>
<div t-if="props.data.fulfilment_type" class="d-flex justify-content-between mb-1">
<span>Fulfilment:</span>
<span class="fw-bold text-uppercase" t-esc="props.data.fulfilment_type_label"/>
</div>
<t t-if="props.data.fulfilment_type === 'delivery'">
<div class="mt-2 pt-2 border-top">
<div class="fw-bold mb-1">DELIVERY ADDRESS:</div>
<div t-if="props.data.delivery_street" t-esc="props.data.delivery_street"/>
<div t-if="props.data.delivery_city || props.data.delivery_zip">
<t t-esc="props.data.delivery_city"/> <t t-esc="props.data.delivery_zip"/>
</div>
<div t-if="props.data.delivery_phone">Phone: <t t-esc="props.data.delivery_phone"/></div>
<div t-if="props.data.delivery_notes" class="mt-1 small" style="font-style: italic;">
Note: <t t-esc="props.data.delivery_notes"/>
</div>
</div>
</t>
<div t-if="props.data.social_ref" class="mt-1 small">
Ref: <t t-esc="props.data.social_ref"/>
</div>
<div t-if="props.data.whatsapp_number" class="mt-1 small">
WhatsApp: <t t-esc="props.data.whatsapp_number"/>
</div>
</div>
<div class="pos-receipt-order-data text-center mt-3" t-if="props.data.footer" style="white-space:pre-line; font-size: 11px;">
<t t-esc="props.data.footer" />
</div>
<div class="pos-receipt-order-data mt-3 text-center" style="font-size: 10px;">
<p>Powered by Dine360</p>
</div>
</div>
</xpath>
</t>
</templates>

View File

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Inherit pos.order list view to show channel fields -->
<record id="view_pos_order_channels_list" model="ir.ui.view">
<field name="name">pos.order.channels.list</field>
<field name="model">pos.order</field>
<field name="inherit_id" ref="point_of_sale.view_pos_order_tree"/>
<field name="arch" type="xml">
<field name="amount_total" position="before">
<field name="order_source" string="Source" widget="badge"
decoration-info="order_source == 'online'"
decoration-warning="order_source == 'phone' or order_source == 'whatsapp'"
decoration-primary="order_source == 'kiosk' or order_source == 'qr'"
decoration-muted="order_source == 'walk_in'"/>
<field name="fulfilment_type" string="Fulfilment" widget="badge"
decoration-success="fulfilment_type == 'pickup'"
decoration-info="fulfilment_type == 'delivery'"
decoration-muted="fulfilment_type == 'dine_in'"/>
</field>
</field>
</record>
<!-- Inherit pos.order form view -->
<record id="view_pos_order_channels_form" model="ir.ui.view">
<field name="name">pos.order.channels.form</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="order_source" string="Order Source" widget="badge"/>
<field name="fulfilment_type" string="Fulfilment" widget="badge"/>
</xpath>
<!-- Delivery section in the Notes tab area -->
<xpath expr="//field[@name='note']" position="before">
<group string="Delivery Details" invisible="fulfilment_type != 'delivery'">
<field name="delivery_partner_id" string="Saved Address"/>
<field name="delivery_street"/>
<field name="delivery_city"/>
<field name="delivery_zip"/>
<field name="delivery_phone"/>
<field name="delivery_notes"/>
</group>
<group string="WhatsApp" invisible="order_source != 'whatsapp'">
<field name="whatsapp_number"/>
</group>
<group string="Social Media" invisible="order_source != 'social_media'">
<field name="social_ref"/>
</group>
</xpath>
</field>
</record>
<!-- POS Config settings - add channel settings to existing POS config form -->
<!-- Note: We skip config view inheritance to avoid xpath issues with pos_self_order -->
<!-- Action: All Orders by Channel -->
<record id="action_pos_orders_by_channel" model="ir.actions.act_window">
<field name="name">Orders by Channel</field>
<field name="res_model">pos.order</field>
<field name="view_mode">tree,form</field>
<field name="context">{'search_default_group_source': 1}</field>
</record>
<!-- Search view extension for group-by Source/Fulfilment -->
<record id="view_pos_order_channels_search" model="ir.ui.view">
<field name="name">pos.order.channels.search</field>
<field name="model">pos.order</field>
<field name="inherit_id" ref="point_of_sale.view_pos_order_filter"/>
<field name="arch" type="xml">
<search>
<field name="order_source"/>
<field name="fulfilment_type"/>
<filter name="filter_phone" string="Phone Orders" domain="[('order_source','=','phone')]"/>
<filter name="filter_whatsapp" string="WhatsApp Orders" domain="[('order_source','=','whatsapp')]"/>
<filter name="filter_online" string="Online Orders" domain="[('order_source','=','online')]"/>
<filter name="filter_kiosk" string="Kiosk Orders" domain="[('order_source','=','kiosk')]"/>
<filter name="filter_delivery" string="Delivery" domain="[('fulfilment_type','=','delivery')]"/>
<filter name="filter_pickup" string="Pickup" domain="[('fulfilment_type','=','pickup')]"/>
<group expand="0" string="Group By">
<filter name="group_source" string="Order Source" context="{'group_by': 'order_source'}"/>
<filter name="group_fulfilment" string="Fulfilment Type" context="{'group_by': 'fulfilment_type'}"/>
</group>
</search>
</field>
</record>
<!-- Menu item in POS menu -->
<menuitem id="menu_pos_orders_by_channel"
name="Orders by Channel"
parent="point_of_sale.menu_point_of_sale"
action="action_pos_orders_by_channel"
sequence="25"/>
</odoo>

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

View File

@ -0,0 +1,18 @@
{
'name': 'Dine360 POS Navbar',
'version': '17.0.1.0',
'category': 'Point of Sale',
'summary': 'Custom POS Navbar mimicking Odoo 19 style',
'depends': ['point_of_sale'],
'data': [],
'assets': {
'point_of_sale._assets_pos': [
'dine360_pos_navbar/static/src/css/pos_navbar.css',
'dine360_pos_navbar/static/src/js/pos_navbar.js',
'dine360_pos_navbar/static/src/xml/pos_navbar.xml',
],
},
'installable': True,
'application': False,
'license': 'LGPL-3',
}

View File

@ -0,0 +1,11 @@
/* Custom POS Navbar styling */
.pos .pos-topheader {
background: inherit;
}
.btn-dashboard:hover {
background: linear-gradient(135deg, #a00d0e 0%, #d61112 100%) !important;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3) !important;
}

View File

@ -0,0 +1,9 @@
/** @odoo-module */
import { Navbar } from "@point_of_sale/app/navbar/navbar";
import { patch } from "@web/core/utils/patch";
// Removing empty/broken setup patch as it was causing the POS to crash.
// Any future navbar customizations should go here.
patch(Navbar.prototype, {
// Other navbar methods can be patched here safely
});

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-inherit="point_of_sale.Navbar" t-inherit-mode="extension" owl="1">
<xpath expr="//img[hasclass('pos-logo')]" position="after">
<a href="/" class="btn-dashboard ms-2 d-flex align-items-center justify-content-center text-white text-decoration-none"
style="background: linear-gradient(135deg, #d61112 0%, #a00d0e 100%); border-radius: 6px; padding: 0 25px; height: 100%; font-weight: 700; border: 1px solid rgba(255,255,255,0.3); transition: all 0.3s ease; box-shadow: 0 2px 5px rgba(0,0,0,0.2);">
<i class="fa fa-th-large me-2"></i>
<span style="font-size: 14px;">Dashboard</span>
</a>
</xpath>
</t>
</templates>

View File

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

View File

@ -0,0 +1,19 @@
{
'name': 'Dine360 QZ Tray Printer',
'version': '1.0',
'category': 'Point of Sale',
'summary': 'Integrate Odoo POS with Star/Epson Printers via QZ Tray.',
'depends': ['point_of_sale'],
'data': [
'views/pos_config_views.xml',
],
'assets': {
'point_of_sale._assets_pos': [
'dine360_qz_printer/static/src/js/qz-tray.js',
'dine360_qz_printer/static/src/js/qz_wrapper.js',
],
},
'installable': True,
'application': False,
'license': 'LGPL-3',
}

View File

@ -0,0 +1,2 @@
from . import pos_config
from . import res_config_settings

View File

@ -0,0 +1,7 @@
from odoo import fields, models
class PosConfig(models.Model):
_inherit = 'pos.config'
use_qz_printer = fields.Boolean("Use QZ Tray Printer", help="Print directly using QZ Tray locally")
qz_printer_name = fields.Char("QZ Printer Name", help="Name of the printer mapped in QZ Tray")

View File

@ -0,0 +1,8 @@
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
use_qz_printer = fields.Boolean(related='pos_config_id.use_qz_printer', readonly=False)
qz_printer_name = fields.Char(related='pos_config_id.qz_printer_name', readonly=False)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,228 @@
/** @odoo-module */
import { patch } from "@web/core/utils/patch";
import { ReceiptScreen } from "@point_of_sale/app/screens/receipt_screen/receipt_screen";
const RECEIPT_COLUMNS = 42;
const NEWLINE = "\r\n";
function normalizeReceiptText(text) {
return (text || "")
.replace(/\u00a0/g, " ")
.replace(/[ \t]+/g, " ")
.split("\n")
.map((line) => line.trim())
.filter(Boolean);
}
function wrapLine(line, width = RECEIPT_COLUMNS) {
if (line.length <= width) {
return [line];
}
const wrapped = [];
let remaining = line;
while (remaining.length > width) {
let breakpoint = remaining.lastIndexOf(" ", width);
if (breakpoint <= 0) {
breakpoint = width;
}
wrapped.push(remaining.slice(0, breakpoint).trimEnd());
remaining = remaining.slice(breakpoint).trimStart();
}
if (remaining) {
wrapped.push(remaining);
}
return wrapped;
}
function money(value, currency) {
const amount = Number(value || 0).toFixed(2);
return currency?.symbol ? `${currency.symbol}${amount}` : amount;
}
function leftRight(left, right, width = RECEIPT_COLUMNS) {
const cleanLeft = String(left || "");
const cleanRight = String(right || "");
const space = Math.max(1, width - cleanLeft.length - cleanRight.length);
return `${cleanLeft}${" ".repeat(space)}${cleanRight}`;
}
function buildReceiptLinesFromOrder(order) {
const currency = order?.pos?.currency;
const lines = [];
const company = order?.pos?.company;
const client = order?.get_partner?.();
const table = order?.table;
const cashier = order?.employee || order?.pos?.get_cashier?.();
// 1. HEADER (Centered-ish)
if (company?.name) {
lines.push(company.name.toUpperCase());
}
if (company?.street) lines.push(company.street);
if (company?.city) lines.push(company.city);
if (company?.phone) lines.push(`Tel: ${company.phone}`);
// Custom Odoo Header
if (order?.pos?.config?.receipt_header) {
lines.push(order.pos.config.receipt_header);
}
lines.push("-".repeat(RECEIPT_COLUMNS));
// 2. ORDER INFO
const receiptNumber = order?.name || "";
if (receiptNumber) lines.push(`Order: ${receiptNumber}`);
if (table) {
lines.push(`TABLE: ${table.name}`.padEnd(20) + `GUESTS: ${order.customer_count || 1}`);
}
if (cashier) {
lines.push(`SERVER: ${cashier.name}`);
}
lines.push(`DATE: ${new Date().toLocaleString()}`);
if (client) {
lines.push(`CUSTOMER: ${client.name}`);
}
lines.push("=".repeat(RECEIPT_COLUMNS));
lines.push(leftRight("ITEM", "PRICE"));
lines.push("-".repeat(RECEIPT_COLUMNS));
// 3. ORDER LINES
for (const orderline of order?.get_orderlines?.() || []) {
const product = orderline.get_product?.();
const name = product?.display_name || product?.name || "";
const qty = orderline.get_quantity?.() || 0;
const priceUnit = orderline.get_unit_display_price?.() || 0;
const total = orderline.get_price_with_tax?.() || 0;
// "Qty x Name" on left, "Total" on right
const itemLabel = `${qty} x ${name}`;
const itemPrice = money(total, currency);
if (itemLabel.length + itemPrice.length + 1 > RECEIPT_COLUMNS) {
lines.push(itemLabel);
lines.push(itemPrice.padStart(RECEIPT_COLUMNS));
} else {
lines.push(leftRight(itemLabel, itemPrice));
}
// Show unit price if qty > 1
if (qty > 1) {
lines.push(` @ ${money(priceUnit, currency)}`);
}
}
// 4. TOTALS
lines.push("=".repeat(RECEIPT_COLUMNS));
lines.push(leftRight("SUBTOTAL", money(order?.get_total_without_tax?.(), currency)));
const tax = order?.get_total_tax?.();
if (tax) {
lines.push(leftRight("TAX", money(tax, currency)));
}
lines.push("-".repeat(RECEIPT_COLUMNS));
lines.push(leftRight("TOTAL", money(order?.get_total_with_tax?.(), currency)));
const change = order?.get_change?.();
if (change) {
lines.push(leftRight("CHANGE", money(change, currency)));
}
lines.push("-".repeat(RECEIPT_COLUMNS));
// Custom Odoo Footer
if (order?.pos?.config?.receipt_footer) {
lines.push(order.pos.config.receipt_footer);
}
lines.push("THANK YOU FOR DINING WITH US!");
lines.push("PLEASE VISIT AGAIN");
lines.push(NEWLINE);
return lines;
}
function buildReceiptLines(receiptElement, order) {
const domLines = normalizeReceiptText(receiptElement?.innerText || "");
if (domLines.length > 1) {
return domLines;
}
return buildReceiptLinesFromOrder(order);
}
function buildEscPosReceipt(receiptElement, order) {
const ESC = "\x1B";
const GS = "\x1D";
let lines = buildReceiptLines(receiptElement, order);
// Safety mechanism: limit receipt length to prevent runaway printing
if (lines.length > 150) {
console.warn("Receipt is suspiciously long, truncating to 150 lines to prevent runaway printing.");
lines.length = 150;
lines.push("-".repeat(RECEIPT_COLUMNS));
lines.push("TRUNCATED FOR SAFETY");
}
const body = lines.flatMap((line) => wrapLine(line)).join(NEWLINE);
console.log("Cutter command run: Preparing ESC/POS data with feed and partial cut.");
return [
ESC + "@", // Initialize printer
ESC + "a" + "\x00", // Left align
body,
NEWLINE + NEWLINE + NEWLINE + NEWLINE + NEWLINE,
GS + "V" + "\x42" + "\x00", // Feed paper to cutting position and perform partial cut (standard ESC/POS)
].join("");
}
patch(ReceiptScreen.prototype, {
async printReceipt() {
if (this.pos.config.use_qz_printer && this.pos.config.qz_printer_name) {
try {
if (!window.qz) {
console.error("QZ Tray library not loaded.");
return false;
}
if (!qz.websocket.isActive()) {
await qz.websocket.connect({ retries: 2, delay: 1 });
}
const printerName = this.pos.config.qz_printer_name;
const config = qz.configs.create(printerName, {
encoding: "CP437",
spool: { end: "\n" },
});
const receiptElement = document.querySelector(".pos-receipt") || document.querySelector(".pos-receipt-container");
if (!receiptElement) {
return false;
}
const printData = buildEscPosReceipt(receiptElement, this.currentOrder);
await qz.print(config, [printData]);
if (this.currentOrder) {
this.currentOrder._printed = true;
}
return true;
} catch (err) {
console.error("QZ Tray Print Error:", err);
this.env.services.popup.add("ErrorPopup", {
title: "QZ Tray Printer Error",
body: "Failed to connect to local QZ Tray or printer. Make sure QZ Tray is running.",
});
return false;
}
} else {
// Fallback to default Odoo print behavior
return super.printReceipt(...arguments);
}
}
});

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="pos_config_view_form_inherit_qz" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.qz</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="96"/>
<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 id="qz_tray_printer" string="QZ Tray Printer (Star/Epson Direct IP Override)" help="Local browser printing via QZ Tray.">
<field name="use_qz_printer"/>
<div class="content-group" invisible="not use_qz_printer">
<div class="row mt16">
<label string="Printer Name" for="qz_printer_name" class="col-lg-3 o_light_label"/>
<field name="qz_printer_name"/>
</div>
</div>
</setting>
</xpath>
</field>
</record>
<record id="pos_config_form_view_inherit_qz" model="ir.ui.view">
<field name="name">pos.config.form.inherit.qz</field>
<field name="model">pos.config</field>
<field name="inherit_id" ref="point_of_sale.pos_config_view_form"/>
<field name="arch" type="xml">
<setting id="other_devices" position="after">
<setting id="qz_tray_printer_pos_config" string="QZ Tray Printer (Star/Epson Direct IP Override)" help="Local browser printing via QZ Tray.">
<field name="use_qz_printer"/>
<div class="content-group" invisible="not use_qz_printer">
<div class="row mt16">
<label string="Printer Name" for="qz_printer_name" class="col-lg-3 o_light_label"/>
<field name="qz_printer_name"/>
</div>
</div>
</setting>
</setting>
</field>
</record>
</odoo>

View File

@ -268,6 +268,70 @@ class TableReservationController(http.Controller):
'end_time': end_time,
'state': 'confirmed' # Direct confirmation from website
})
# Send Emails
try:
import logging
_logger = logging.getLogger(__name__)
# Use the configured outgoing mail server's FROM address (must match SMTP username)
outgoing_server = request.env['ir.mail_server'].sudo().search([], limit=1)
smtp_from = outgoing_server.smtp_user if outgoing_server else 'alaguraj0361@gmail.com'
# 1. Notify the Company (Admin)
admin_mail_values = {
'subject': f"New Table Reservation: {customer_name}",
'body_html': f"""
<div style="font-family: Arial, sans-serif; padding: 20px;">
<h2 style="color: #2BB1A5;">New Table Reservation</h2>
<p>A new table reservation has been submitted from the website.</p>
<table style="width:100%; border-collapse: collapse;">
<tr><td style="padding:8px; font-weight:bold;">Customer Name:</td><td style="padding:8px;">{customer_name}</td></tr>
<tr><td style="padding:8px; font-weight:bold;">Phone:</td><td style="padding:8px;">{phone}</td></tr>
<tr><td style="padding:8px; font-weight:bold;">Email:</td><td style="padding:8px;">{email}</td></tr>
<tr><td style="padding:8px; font-weight:bold;">Number of Guests:</td><td style="padding:8px;">{num_people}</td></tr>
<tr><td style="padding:8px; font-weight:bold;">Reservation Time:</td><td style="padding:8px;">{start_time_str}</td></tr>
</table>
</div>
""",
'email_to': 'alaguraj0361@gmail.com',
'email_from': smtp_from,
'reply_to': email,
}
admin_mail = request.env['mail.mail'].sudo().create(admin_mail_values)
admin_mail.send()
_logger.info("Admin reservation notification sent to alaguraj0361@gmail.com")
# 2. Notify the Customer
customer_mail_values = {
'subject': "Reservation Confirmation - Chennora",
'body_html': f"""
<div style="font-family: Arial, sans-serif; padding: 20px;">
<h2 style="color: #2BB1A5;">Reservation Confirmed!</h2>
<p>Dear {customer_name},</p>
<p>Your table reservation at <b>Chennora Indian Kitchen Bar</b> has been confirmed successfully!</p>
<table style="width:100%; border-collapse: collapse; margin-top: 15px;">
<tr><td style="padding:8px; font-weight:bold;">Number of Guests:</td><td style="padding:8px;">{num_people}</td></tr>
<tr><td style="padding:8px; font-weight:bold;">Reservation Time:</td><td style="padding:8px;">{start_time_str}</td></tr>
</table>
<p style="margin-top:20px;">If you need to make any changes or have questions, please reply to this email or call us at <b>+1(647)856-2878</b>.</p>
<p>We look forward to seeing you!</p>
<p>Thank you,<br><b>Chennora Indian Kitchen Bar</b></p>
</div>
""",
'email_to': email,
'email_from': smtp_from,
'reply_to': smtp_from,
}
customer_mail = request.env['mail.mail'].sudo().create(customer_mail_values)
customer_mail.send()
_logger.info("Customer reservation confirmation sent to %s", email)
except Exception as e:
import logging
_logger = logging.getLogger(__name__)
_logger.error("Failed to send reservation emails: %s", str(e))
return request.render("dine360_reservation.reservation_success_template", {
'reservation': reservation,
})

Some files were not shown because too many files have changed in this diff Show More