diff --git a/addons/pos_restaurant/data/pos_restaurant_demo.xml b/addons/pos_restaurant/data/pos_restaurant_demo.xml
index b073911b094..e25964359eb 100644
--- a/addons/pos_restaurant/data/pos_restaurant_demo.xml
+++ b/addons/pos_restaurant/data/pos_restaurant_demo.xml
@@ -196,7 +196,6 @@
-
Main Floorrgb(136,137,242)
diff --git a/addons/pos_self_order/__manifest__.py b/addons/pos_self_order/__manifest__.py
index 713db7c505d..cee81b3a640 100644
--- a/addons/pos_self_order/__manifest__.py
+++ b/addons/pos_self_order/__manifest__.py
@@ -9,13 +9,13 @@
"auto_install": ["pos_restaurant"],
"demo": [
"demo/pos_restaurant_demo.xml",
+ "demo/custom_link_data.xml",
],
"data": [
"security/ir.model.access.csv",
"views/index.xml",
"views/qr_code.xml",
"views/custom_link_views.xml",
- "data/custom_link_data.xml",
"data/restaurant_table_data.xml",
"views/res_config_settings_views.xml",
"views/point_of_sale_dashboard.xml",
@@ -35,6 +35,8 @@
"web/static/src/session.js",
"web/static/src/core/utils/transitions.scss",
"web/static/src/core/**/*",
+ ("remove", "web/static/src/core/browser/router_service.js"),
+ ("remove", "web/static/src/core/debug/**/*"),
"web/static/lib/owl/owl.js",
"web/static/lib/owl/odoo_module.js",
"web/static/lib/luxon/luxon.js",
@@ -43,6 +45,11 @@
"web/static/lib/odoo_ui_icons/*",
"pos_self_order/static/src/**/*",
"point_of_sale/static/src/utils.js",
+ # bus service
+ 'bus/static/src/services/bus_service.js',
+ 'bus/static/src/bus_parameters_service.js',
+ 'bus/static/src/multi_tab_service.js',
+ 'bus/static/src/workers/*',
],
"pos_self_order.assets_tests": [
"web/static/lib/jquery/jquery.js",
diff --git a/addons/pos_self_order/controllers/__init__.py b/addons/pos_self_order/controllers/__init__.py
index 7d91bf1afc7..0aa6faafeb3 100644
--- a/addons/pos_self_order/controllers/__init__.py
+++ b/addons/pos_self_order/controllers/__init__.py
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
-from . import qr_menu
-from . import ordering
+from . import entry
+from . import orders
diff --git a/addons/pos_self_order/controllers/qr_menu.py b/addons/pos_self_order/controllers/entry.py
similarity index 67%
rename from addons/pos_self_order/controllers/qr_menu.py
rename to addons/pos_self_order/controllers/entry.py
index 7601e573442..a065183677d 100644
--- a/addons/pos_self_order/controllers/qr_menu.py
+++ b/addons/pos_self_order/controllers/entry.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
-from typing import Dict, Optional
+from typing import Optional
import werkzeug
from odoo import http
@@ -11,8 +11,6 @@ from odoo.addons.pos_self_order.controllers.utils import (
get_any_pos_config_sudo,
get_table_sudo,
)
-from odoo.addons.pos_self_order.models.pos_config import PosConfig
-from odoo.addons.pos_self_order.models.pos_restaurant import RestaurantTable
class PosQRMenuController(http.Controller):
@@ -30,41 +28,46 @@ class PosQRMenuController(http.Controller):
return request.redirect(f"/menu/{get_any_pos_config_sudo().id}")
@http.route(
- [
- "/menu/",
- "/menu//products",
- "/menu//products/",
- "/menu//cart",
- "/menu//orders",
- ],
+ [
+ "/menu/",
+ "/menu//"
+ ],
auth="public",
website=True,
sitemap=True,
)
- def pos_self_order_start(
- self, pos_name: str, at: Optional[str] = None, product_id: Optional[int] = None
- ):
+ def pos_self_order_start(self, config_id: str, at=None):
"""
The user gets this route from the QR code that they scan at the table
- :param pos_name: the name of the pos config: can be the id or the slugified name of the pos config. (e.g. "3" or "bar-3")
+ :param config_id: the name of the pos config: can be the id or the slugified name of the pos config. (e.g. "3" or "bar-3")
:param at: the access token of the table; we call this argument "at" because
- it will be displayed in the url ( as a query param ), and "at" is more user friendly than "table_access_token"
+ it will be displayed in the url ( as a query param ), and "at" is more user friendly than "access_token"
the user is allowed to order only if this "at" matches the access token of a table
:param product_id: the id of the product that the user wants to see the details of;
we never actually use this argument in this function ( it will be read by the client side router ),
but we still have it here, because otherwise we get a Warning in the logs
:return: the rendered template
"""
+ config_sudo = get_pos_config_sudo(config_id)
+ table_sudo = get_table_sudo(access_token=at)
+ self_order_mode = 'qr_code'
+ table_infos = False
+
+ if config_sudo.has_active_session and config_sudo.self_order_table_mode and table_sudo:
+ table_infos = table_sudo._get_self_order_data()
+ self_order_mode = config_sudo.self_order_pay_after
+
return request.render(
- "pos_self_order.index",
+ 'pos_self_order.index',
{
- "session_info": {
+ 'session_info': {
**request.env["ir.http"].get_frontend_session_info(),
- "currencies": request.env["ir.http"].get_currencies(),
- "pos_self_order_data": self._get_self_order_config(
- get_pos_config_sudo(pos_name),
- get_table_sudo(table_access_token=at),
- ),
+ 'currencies': request.env["ir.http"].get_currencies(),
+ 'pos_self_order_data': {
+ 'self_order_mode': self_order_mode,
+ 'table': table_infos,
+ **config_sudo._get_self_order_data(),
+ },
}
},
)
@@ -116,21 +119,3 @@ class PosQRMenuController(http.Controller):
._get_image_stream_from(pos_config_sudo, field_name="self_order_image")
.get_response()
)
-
- def _get_self_order_config(
- self, pos_config_sudo: PosConfig, table_sudo: RestaurantTable
- ) -> Dict:
- """
- Returns the necessary information for the POS Self Order App
- """
- return {
- **pos_config_sudo._get_self_order_data(),
- **(
- table_sudo
- and pos_config_sudo.has_active_session
- and pos_config_sudo.self_order_table_mode
- # having the table_id in the frontend means that the client will be able to order
- and {"table": table_sudo._get_self_order_data()}
- or {}
- ),
- }
diff --git a/addons/pos_self_order/controllers/ordering.py b/addons/pos_self_order/controllers/ordering.py
deleted file mode 100644
index 903fd58f571..00000000000
--- a/addons/pos_self_order/controllers/ordering.py
+++ /dev/null
@@ -1,464 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from typing import List, Dict, Optional, Union
-import werkzeug
-import uuid
-
-from odoo import http, fields
-from odoo.http import request
-from odoo.tools import partition
-
-
-from odoo.addons.pos_self_order.controllers.utils import (
- get_pos_config_sudo,
- get_table_sudo,
-)
-from odoo.addons.pos_self_order.models.product_product import ProductProduct
-from odoo.addons.pos_self_order.models.pos_order import PosOrder, PosOrderLine
-from odoo.addons.pos_self_order.models.pos_config import PosConfig
-
-
-class PosSelfOrderController(http.Controller):
- @http.route(
- "/pos-self-order/send-order",
- auth="public",
- type="json",
- website=True,
- )
- def pos_self_order_send_order(
- self,
- cart: List[Dict],
- pos_config_id: int,
- table_access_token: Optional[str] = None,
- order_pos_reference: Optional[str] = None,
- order_access_token: Optional[str] = None,
- ) -> Dict[str, str]:
- """
- There are 2 types of self order configurations:
- 1. Order at table and pay after each order
- ==> pos doesn't allow ongoing orders
- 2. Order at table and pay at the end of the meal
- ==> pos allows ongoing orders
- (order at table means order from personal device (smartphone))
- In case 1. we will create a new order each time the user sends an order
- In case 2. we will create a new order only if the user does not have an ongoing order
- (we will look whether there is an order with state = 'draft'
- with the same pos_reference and order_access_token as the ones that the user has provided)
- :param cart: order lines
- :param pos_config_id: the id of the POS where the order is being sent
- :param table_access_token: the access token of the table where the order is being sent (UUID v4)
- :param pos_reference: the id of the order that is being edited; ex: "Order 00001-001-0001"
- :param order_access_token: the access token of the order that is being edited (UUID v4)
- :return: dictionary with keys: pos_reference, order_access_token
- """
- # we will only respond with the order "access_token" and "id". After clicking on the `Order` button,
- # the customer will be redirected to the landing page.
- # From the `landing page`, the customer can go to the `My Orders` page.
- # on this page, the app will automatically make a request to the `view-order` route,
- # so the customer will get the order details that way
-
- pos_config_sudo = get_pos_config_sudo(pos_config_id)
-
- if not pos_config_sudo.self_order_table_mode or not pos_config_sudo.has_active_session:
- raise werkzeug.exceptions.BadRequest()
-
- order = self._form_order(
- self._create_order_data(
- cart,
- pos_config_sudo,
- table_access_token,
- order_pos_reference,
- order_access_token,
- )
- )
- posted_order_id = (
- request.env["pos.order"]
- .sudo()
- .create_from_ui(
- [order],
- draft=True,
- )[0]
- .get("id")
- )
-
- return (
- request.env["pos.order"]
- .sudo()
- .browse(posted_order_id)
- .read(["pos_reference", "access_token"])[0]
- )
-
- # # is_trusted is set to True by default.
- # # We need to set it to False, because we are creating an order from a public route
- # # FIXME: make it so we only set it to false if it is a new order
- # # if the server already acknowledged the order, we should not set it to false again
- # # every time the user adds a new item to the order
- # request.env["pos.order"].sudo().browse(order_resp.get("id")).is_trusted = False
-
- @http.route("/pos-self-order/view-order", auth="public", type="json", website=True)
- def pos_self_order_view_order(self, pos_reference: str, access_token: str) -> Dict:
- """
- Return some information about a given order.
- This is used by the frontend to find the latest state of an order.
- (e.g. if a customer orders something from the self order app and then the waiter adds an item to the order,
- the customer will be able to see the new item in the order)
- :param pos_reference: the id of the order that we want to view
- :param access_token: the access token of the order that we want to view -- this is needed
- for security reasons, so that only the customer who created the order
- can view it
- """
- order_sudo = self._find_order(pos_reference, access_token)
- if not order_sudo:
- raise werkzeug.exceptions.Unauthorized()
- return order_sudo._export_for_self_order()
-
- def _create_order_data(
- self,
- cart: List[Dict],
- pos_config_sudo: PosConfig,
- table_access_token: Optional[str],
- order_pos_reference: Optional[str],
- order_access_token: Optional[str],
- ) -> Dict[str, Union[int, str, List[Dict]]]:
- """
- Given the data, we decide whether we need to create a new order or update an existing one
- :return: dictionary with the data needed to create a new order
- """
-
- # We only care about the previous order if the customer is paying after the meal
- # ( paying after the meal means that the customer can order multiple times on the same order )
- order_sudo = pos_config_sudo.self_order_pay_after == "meal" and self._find_order(
- order_pos_reference, order_access_token, state="draft"
- )
-
- return {
- **(
- order_sudo._get_self_order_data()
- if order_sudo
- else self._create_new_order_data(pos_config_sudo, table_access_token)
- ),
- "lines": self._create_orderlines(
- self._merge_orderlines(
- cart, order_sudo.lines.export_for_ui(), order_sudo.config_id
- )
- if order_sudo
- else cart,
- pos_config_sudo,
- ),
- }
-
- def _create_new_order_data(
- self, pos_config_sudo: PosConfig, table_access_token: str
- ) -> Dict[str, Union[str, int]]:
- """
- :param pos_config_id: the id of the pos config for which we want to create the order
- :param table_access_token: the access token of the table for which we want to create the order
- :return: a dictionary containing the data that we need to create a new order
- """
- table_sudo = get_table_sudo(table_access_token)
- pos_session_sudo = pos_config_sudo.current_session_id
- if not table_sudo or not pos_session_sudo:
- raise werkzeug.exceptions.Unauthorized()
-
- sequence_number = self._get_sequence_number(table_sudo.id, pos_session_sudo.id)
-
- return {
- "id": self._generate_unique_id(pos_session_sudo.id, table_sudo.id, sequence_number),
- "sequence_number": sequence_number,
- "access_token": uuid.uuid4().hex,
- "session_id": pos_session_sudo.id,
- "table_id": table_sudo.id,
- }
-
- def _merge_orderlines(
- self,
- incoming_orderlines: List[Dict],
- existing_orderlines: List[Dict],
- pos_config_sudo: PosConfig,
- ) -> List[Dict]:
- """
- If the customer has an existing order, we will add the items from the existing order to the current cart.
- (This is because the create_from_ui method will overwrite the old items from the order)
- :param cart: The cart from the frontend.
- :return: list of dictionaries with the items from the cart and the items from the existing order
- """
- has_merge_candidate = lambda item: self._has_merge_candidate(item, existing_orderlines)
-
- orderlines_to_be_merged, orderlines_to_be_appended = partition(
- has_merge_candidate, incoming_orderlines
- )
-
- return (
- self._get_updated_orderlines(
- existing_orderlines, orderlines_to_be_merged, pos_config_sudo
- )
- + orderlines_to_be_appended
- )
-
- def _has_merge_candidate(self, item: Dict, existing_orderlines: List[Dict]) -> bool:
- return any(self._can_be_merged(item, line) for line in existing_orderlines)
-
- def _can_be_merged(self, orderline1, orderline2):
- return self._is_pos_groupable(orderline1["product_id"]) and all(
- orderline1.get(key, "") == orderline2.get(key, "")
- for key in PosOrderLine._get_unique_keys()
- )
-
- def _get_updated_orderlines(
- self, orderlines: List[Dict], cart: List[Dict], pos_config_sudo: PosConfig
- ) -> List[Dict]:
- """
- :returns updated version of orderlines that takes into account the new items from the cart
- """
- return [
- self._get_updated_orderline(line, updated_item, pos_config_sudo)
- if (
- updated_item := next(
- (item for item in cart if self._can_be_merged(item, line)), None
- )
- )
- else line
- for line in orderlines
- ]
-
- def _get_updated_orderline(
- self, line: Dict, updated_item: Dict, pos_config_sudo: PosConfig
- ) -> Dict:
- """
- This function assumes that the price of the product was not changed between the time the
- first items were added to the order and the time the new items were added to the order.
- """
- new_qty = updated_item["qty"] + line["qty"]
- new_price = (
- request.env["product.product"]
- .sudo()
- .browse(int(line["product_id"]))
- ._get_price_info(pos_config_sudo, qty=new_qty)
- )
- return {
- **line,
- **{
- "price_subtotal": new_price["price_without_tax"],
- "price_subtotal_incl": new_price["price_with_tax"],
- "qty": new_qty,
- "customer_note": updated_item.get("customer_note") or line.get("customer_note"),
- },
- }
-
- def _is_pos_groupable(self, product_id: int) -> bool:
- return (
- request.env["product.product"].sudo().browse(int(product_id)).uom_id.is_pos_groupable
- )
-
- def _get_full_product_name(self, name: str, description: str) -> str:
- """
- :param name: ex: "[E-COM12] Desk Organizer"
- :param description: ex: "M, Leather"
- :return: ex: "Desk Organizer (M, Leather)"
- """
- return f"{name} ({description})" if description else name
-
- def _create_orderlines(self, cart: List[Dict], pos_config_sudo: PosConfig) -> List[Dict]:
- """
- Function that constructs the order lines (as the create_from_ui method expects them) from the cart.
- From the frontend we only get basic data of the cart, such as the id of the product and the quantity.
- We need to get the other details of the product from the database.
- This is done for security reasons.
- """
-
- return [
- [
- 0,
- 0,
- self._create_orderline(
- item,
- pos_config_sudo,
- ),
- ]
- # having the "full product name" means that the orderline is already in the db
- # so we don't need to create a new object
- if not item.get("full_product_name") else [0, 0, item]
- for item in cart
- ]
-
- def _create_orderline(self, item: Dict, pos_config_sudo: PosConfig) -> Dict:
- """
- Function that constructs an order line (as the create_from_ui method expects it) from an item from the cart.
- """
- product_sudo = request.env["product.product"].sudo().browse(int(item.get("product_id")))
- return {
- "product_id": item.get("product_id"),
- "qty": item.get("qty"),
- **self._get_orderline_price_info(product_sudo, item, pos_config_sudo),
- "discount": 0,
- "tax_ids": product_sudo.taxes_id,
- "pack_lot_ids": [],
- "description": item.get("description"), # ex "M, Leather"
- "full_product_name": self._get_full_product_name( # ex: "Desk Organizer (M, Leather)"
- product_sudo._get_name(),
- item.get("description"),
- ),
- "customer_note": item.get("customer_note"),
- "pricec_type": "original",
- "note": "",
- "uuid": str(uuid.uuid4()),
- }
-
- def _get_orderline_price_info(
- self, product_sudo: ProductProduct, item: Dict, pos_config_sudo: PosConfig
- ) -> Dict:
- price_extra = (
- self._compute_price_extra(product_sudo, item.get("description"), pos_config_sudo)
- if item.get("description")
- else 0
- )
- price_unit = product_sudo.lst_price + price_extra
- price_subtotal_info = product_sudo._get_price_info(
- pos_config_sudo, price_unit, item.get("qty")
- )
-
- return {
- "price_unit": price_unit,
- "price_subtotal": price_subtotal_info["price_without_tax"],
- "price_subtotal_incl": price_subtotal_info["price_with_tax"],
- "price_extra": price_extra,
- }
-
- def _compute_price_extra(
- self, product_sudo: ProductProduct, description: str, pos_config_sudo: PosConfig
- ) -> float:
- """
- Function that computes the price_extra of a product based on the description.
- :param description: The description of the product. ex: "M, Leather"
- represents the selected values of the product's attributes.
- """
-
- return sum(
- v["price_extra"]["price_without_tax"]
- for attr, selected_value_name in zip(
- product_sudo._get_attributes(pos_config_sudo), description.split(", ")
- )
- for v in attr["values"]
- if v["name"] == selected_value_name
- )
-
- def _form_order(self, order: Dict) -> Dict:
- """
- Function that constructs an order object (as the create_from_ui method expects it)
- It takes as input the order object that was created by self._create_order_data()
- :param order: Dictionary representing an order.
- :return: Dictionary representing an order (as the create_from_ui method expects it).
- """
- # The self order can only update an order that was created on the self order itself.
- # This is because the self order does not have the ability to update orders that were created on the POS,
- # because the app wouldn't know the access token of the order.
-
- return {
- "id": order.get("id"),
- "data": {
- "name": order.get("id"),
- "amount_paid": 0,
- "amount_total": self._compute_amount_total(order.get("lines")),
- "amount_tax": self._compute_amount_tax(order.get("lines")),
- "amount_return": 0,
- "lines": order.get("lines"),
- "statement_ids": [],
- "pos_session_id": order.get("session_id"),
- "partner_id": False,
- "user_id": request.session.uid,
- # we only need the digits of the pos_reference, not the whole id, which starts with word "Order"
- "uid": order.get("id")[6:],
- "sequence_number": order.get("sequence_number"),
- "creation_date": str(fields.Datetime.now()),
- "fiscal_position_id": False,
- "to_invoice": False,
- "to_ship": False,
- "is_tipped": False,
- "tip_amount": 0,
- "access_token": order.get("access_token"),
- "server_id": False,
- "table_id": order.get("table_id"),
- },
- "to_invoice": False,
- "session_id": order.get("session_id"),
- }
-
- def _compute_amount_total(self, lines: List[Dict]) -> float:
- return sum(orderline[2].get("price_subtotal_incl") for orderline in lines)
-
- def _compute_amount_tax(self, lines: List[Dict]) -> float:
- return sum(
- orderline[2].get("price_subtotal_incl") - orderline[2].get("price_subtotal")
- for orderline in lines
- )
-
- # the 2nd argument of pos_reference is the login number;
- # the regular pos receives the login number from the backend on the first load
- # the problem is that if the pos has login number 1, for example
- # and we create a new order in the database, with sequence number 1, for example,
- # when the regular pos will try to create a new order, it will also use sequence number 1
- # and login number 1 and the new order will have the same id as the order created by the self order app
- # so it will ultimately not be recorded in the database.
- # ( the regular pos does not check what was the last order id from the database )
- # it keeps track itself of the last order id -- this is why this problem occurs
- # I thought that it is best to make the pos_self_order app work around
- # the problem, instead of modifying the regular pos.
- # so I decided to use the numbers from 900 to 999 for the login number
- # this way, the regular pos will have a login number of 1, 2, 3, etc
- # and the pos_self_order app will have a login number of 901, 902, 903, etc,
- # where 901 is the login number of the table with id = 1,
- # 902 is the login number of the table with id = 2, etc
- # TODO: make it so the regular pos can't have a login number of 900 or more
- # FIXME: what happens if `table_id` is larger that 100?
- # [possible fix: using a different pos session for the self order an synchronizing the orders]
- def _generate_unique_id(self, pos_session_id: int, table_id: int, sequence_number: int) -> str:
- """
- Generates a public identification number for the order.
- :return: The order name. Example: Order 00001-001-0001
- """
- return f"Order {pos_session_id:0>5}-{900 + table_id :0>3}-{sequence_number:0>4}"
-
- def _find_order(
- self,
- pos_reference: Optional[str],
- order_access_token: Optional[str],
- state: Optional[str] = None,
- ) -> PosOrder:
- """
- :return: pos order with the given pos_reference, access token and state
- """
- return (
- pos_reference
- and order_access_token
- and request.env["pos.order"]
- .sudo()
- .search(
- [
- ("pos_reference", "=", pos_reference),
- ("access_token", "=", order_access_token),
- *(state and [("state", "=", state)] or []),
- ],
- limit=1,
- )
- )
-
- def _get_sequence_number(self, table_id: int, session_id: int) -> int:
- """
- :return: the new sequence number for the order.
- """
- return (
- request.env["pos.order"]
- .sudo()
- .search(
- [
- (
- "pos_reference",
- "=like",
- f"Order {session_id:0>5}-{900 + table_id}-____",
- ),
- ],
- limit=1,
- )
- .sequence_number
- + 1
- ) or 1
diff --git a/addons/pos_self_order/controllers/orders.py b/addons/pos_self_order/controllers/orders.py
new file mode 100644
index 00000000000..e5144c947e3
--- /dev/null
+++ b/addons/pos_self_order/controllers/orders.py
@@ -0,0 +1,211 @@
+# -*- coding: utf-8 -*-
+
+import uuid
+
+from odoo import http, fields, Command
+from odoo.http import request
+from odoo.addons.pos_self_order.controllers.utils import (
+ get_pos_config_sudo,
+ get_table_sudo,
+)
+
+from werkzeug.exceptions import NotFound, BadRequest, Unauthorized
+
+class PosSelfOrderController(http.Controller):
+ @http.route("/pos-self-order/process-new-order", auth="public", type="json", website=True)
+ def process_new_order(self, order, table_access_token):
+ pos_config_id = order.get('pos_config_id')
+ lines = order.get('lines')
+
+ pos_config_sudo = get_pos_config_sudo(pos_config_id)
+ pos_session_sudo = pos_config_sudo.current_session_id
+ table_sudo = get_table_sudo(table_access_token)
+
+ if not pos_config_sudo.self_order_table_mode or not pos_config_sudo.has_active_session:
+ raise Unauthorized
+ if not table_sudo or not pos_session_sudo:
+ raise Unauthorized
+
+ sequence_number = self._get_sequence_number(table_sudo.id, pos_session_sudo.id)
+ unique_id = self._generate_unique_id(pos_session_sudo.id, table_sudo.id, sequence_number)
+
+ # Create the order without lines and prices computed
+ # We need to remap the order because some required fields are not used in the frontend.
+ order = {
+ 'id': unique_id,
+ 'data': {
+ 'uuid': order.get('uuid'),
+ 'name': unique_id,
+ 'user_id': request.session.uid,
+ 'sequence_number': sequence_number,
+ 'access_token': uuid.uuid4().hex,
+ 'pos_session_id': pos_session_sudo.id,
+ 'table_id': table_sudo.id,
+ "partner_id": False,
+ "creation_date": str(fields.Datetime.now()),
+ "fiscal_position_id": pos_config_sudo.default_fiscal_position_id,
+ "statement_ids": [],
+ "lines": [],
+ 'amount_tax': 0,
+ 'amount_total': 0,
+ 'amount_paid': 0,
+ 'amount_return': 0,
+ },
+ "to_invoice": False,
+ 'session_id': pos_session_sudo.id,
+ }
+
+ # Save the order in the database to get the id
+ posted_order_id = request.env['pos.order'].sudo().create_from_ui([order], draft=True)[0].get('id')
+
+ # Process the lines and get their prices computed
+ lines = self._process_lines(lines, pos_config_sudo, posted_order_id)
+
+ # Compute the order prices
+ amount_total, amount_untaxed = self._get_order_prices(lines)
+
+ # Update the order with the computed prices and lines
+ order_sudo = request.env["pos.order"].sudo().browse(posted_order_id)
+ order_sudo.write({
+ 'lines': [Command.create(line) for line in lines],
+ 'amount_tax': amount_total - amount_untaxed,
+ 'amount_total': amount_total,
+ })
+
+ return order_sudo._export_for_self_order()
+
+ @http.route('/pos-self-order/get-orders-taxes', auth='public', type='json', website=True)
+ def get_order_taxes(self, order, pos_config_id):
+ pos_config_sudo = get_pos_config_sudo(pos_config_id)
+
+ if not pos_config_sudo or not pos_config_sudo.self_order_table_mode:
+ raise Unauthorized
+
+ lines = self._process_lines(order.get('lines'), pos_config_sudo, 0)
+ amount_total, amount_untaxed = self._get_order_prices(lines)
+
+ return {
+ 'lines': [{
+ 'uuid': line.get('uuid'),
+ 'price_subtotal': line.get('price_subtotal'),
+ 'price_subtotal_incl': line.get('price_subtotal_incl'),
+ } for line in lines],
+ 'amount_total': amount_total,
+ 'amount_tax': amount_total - amount_untaxed,
+ }
+
+ @http.route('/pos-self-order/update-existing-order', auth="public", type="json", website=True)
+ def update_existing_order(self, order):
+ order_pos_reference = order.get('pos_reference')
+ order_access_token = order.get('access_token')
+ pos_config_id = order.get('pos_config_id')
+ pos_config_sudo = get_pos_config_sudo(pos_config_id)
+
+ order_sudo = request.env['pos.order'].sudo().search([
+ ('pos_reference', '=', order_pos_reference),
+ ('access_token', '=', order_access_token),
+ ])
+
+ if not order_sudo:
+ raise Unauthorized("Order not found in the server !")
+ elif order_sudo.state != 'draft':
+ raise BadRequest("Order is not in draft state")
+
+ lines = self._process_lines(order.get('lines'), pos_config_sudo, order_sudo.id)
+ for line in lines:
+ if line.get('id'):
+ line_sudo = order_sudo.lines.filtered(lambda l: l.id == line.get('id'))
+
+ if line.get('qty') < line_sudo.qty:
+ line.set('qty', line_sudo.qty)
+
+ line_sudo.write({
+ **line,
+ })
+ else:
+ order_sudo.lines.create(line)
+
+ amount_total, amount_untaxed = self._get_order_prices(lines)
+ order_sudo.write({
+ 'amount_tax': amount_total - amount_untaxed,
+ 'amount_total': amount_total,
+ })
+
+ return order_sudo._export_for_self_order()
+
+ @http.route('/pos-self-order/get-orders', auth='public', type='json', website=True)
+ def get_orders_by_access_token(self, access_tokens):
+ orders_sudo = request.env["pos.order"].sudo().search([
+ ("access_token", "in", access_tokens),
+ ])
+
+ if not orders_sudo:
+ raise NotFound()
+
+ orders = []
+ for order in orders_sudo:
+ orders.append(order._export_for_self_order())
+
+ return orders
+
+ def _process_lines(self, lines, pos_config_sudo, pos_order_id):
+ newLines = []
+ pricelist = request.env['product.pricelist'].sudo().browse(pos_config_sudo.pricelist_id.id)
+
+ for line in lines:
+ product_sudo = request.env["product.product"].sudo().browse(int(line.get("product_id")))
+ # todo take into account the price extra
+ price_unit = pricelist._get_product_price(product_sudo, quantity=1) if pricelist else product_sudo.lst_price
+
+ config_fiscal_pos = pos_config_sudo.default_fiscal_position_id
+ selected_account_tax = config_fiscal_pos.map_tax(product_sudo.taxes_id) if config_fiscal_pos else product_sudo.taxes_id
+
+ tax_results = selected_account_tax.compute_all(
+ price_unit,
+ pos_config_sudo.currency_id,
+ line.get('qty'),
+ product_sudo,
+ )
+
+ newLines.append({
+ 'price_unit': price_unit,
+ 'price_subtotal': tax_results.get('total_excluded'),
+ 'price_subtotal_incl': tax_results.get('total_included'),
+ 'price_extra': 0,
+ 'id': line.get('id'),
+ 'order_id': pos_order_id,
+ 'tax_ids': product_sudo.taxes_id,
+ 'uuid': line.get('uuid'),
+ 'product_id': line.get('product_id'),
+ 'qty': line.get('qty'),
+ 'customer_note': line.get('customer_note'),
+ 'selected_attributes': line.get('selected_attributes'),
+ 'full_product_name': line.get('full_product_name'),
+ })
+
+ return newLines
+
+ def _get_order_prices(self, lines):
+ amount_untaxed = sum([line.get('price_subtotal') for line in lines])
+ amount_total = sum([line.get('price_subtotal_incl') for line in lines])
+ return amount_total, amount_untaxed
+
+ # The first part will be the session_id of the order.
+ # The second part will be the table_id of the order.
+ # Last part the sequence number of the order.
+ # INFO: This is allow a maximum of 999 tables and 9999 orders per table, so about ~1M orders per session.
+ # Example: 'Self-Order 00001-001-0001'
+ def _generate_unique_id(self, pos_session_id: int, table_id: int, sequence_number: int) -> str:
+ first_part = "{:05d}".format(int(pos_session_id))
+ second_part = "{:03d}".format(int(table_id))
+ third_part = "{:04d}".format(int(sequence_number))
+
+ return f"Self-Order {first_part}-{second_part}-{third_part}"
+
+ def _get_sequence_number(self, table_id: int, session_id: int) -> int:
+ order_sudo = request.env["pos.order"].sudo().search([(
+ 'pos_reference',
+ 'like',
+ f"Self-Order {session_id:0>5}-{table_id:0>3}")], order='id desc', limit=1)
+
+ return (order_sudo.sequence_number + 1) or 1
diff --git a/addons/pos_self_order/controllers/utils.py b/addons/pos_self_order/controllers/utils.py
index b2f8463a3d1..07e6296b096 100644
--- a/addons/pos_self_order/controllers/utils.py
+++ b/addons/pos_self_order/controllers/utils.py
@@ -39,11 +39,11 @@ def get_any_pos_config_sudo() -> PosConfig:
) or _raise(werkzeug.exceptions.NotFound())
-def get_table_sudo(table_access_token: Optional[str]) -> Optional[RestaurantTable]:
- return table_access_token and (
+def get_table_sudo(access_token: Optional[str]) -> Optional[RestaurantTable]:
+ return access_token and (
request.env["restaurant.table"]
.sudo()
- .search([("access_token", "=", table_access_token), ("active", "=", True)], limit=1)
+ .search([("access_token", "=", access_token), ("active", "=", True)], limit=1)
)
diff --git a/addons/pos_self_order/data/custom_link_data.xml b/addons/pos_self_order/data/custom_link_data.xml
deleted file mode 100644
index 1a440a52e0b..00000000000
--- a/addons/pos_self_order/data/custom_link_data.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
- View Menu
- /products
-
-
-
diff --git a/addons/pos_self_order/demo/custom_link_data.xml b/addons/pos_self_order/demo/custom_link_data.xml
new file mode 100644
index 00000000000..e66ccc69535
--- /dev/null
+++ b/addons/pos_self_order/demo/custom_link_data.xml
@@ -0,0 +1,10 @@
+
+
+
+
+ View Menu
+
+
+
+
+
diff --git a/addons/pos_self_order/demo/pos_restaurant_demo.xml b/addons/pos_self_order/demo/pos_restaurant_demo.xml
index 30de26cfd9e..4ce760cfa1f 100644
--- a/addons/pos_self_order/demo/pos_restaurant_demo.xml
+++ b/addons/pos_self_order/demo/pos_restaurant_demo.xml
@@ -4,12 +4,12 @@
TrueTrue
-
-
diff --git a/addons/pos_self_order/models/pos_config.py b/addons/pos_self_order/models/pos_config.py
index 984c72d6ebd..12ab9faca1a 100644
--- a/addons/pos_self_order/models/pos_config.py
+++ b/addons/pos_self_order/models/pos_config.py
@@ -61,15 +61,21 @@ class PosConfig(models.Model):
directly on the fields, but `module_pos_restaurant` would not be
known at the time that the function for this default value would run)
"""
- for vals in vals_list:
- if vals.get("module_pos_restaurant"):
- vals.update(
- {
- "self_order_view_mode": True,
- "self_order_table_mode": True,
- }
- )
- return super().create(vals_list)
+ pos_config_ids = super().create(vals_list)
+
+ for pos_config_id in pos_config_ids:
+ if pos_config_id.module_pos_restaurant:
+ pos_config_id.self_order_view_mode = True
+ pos_config_id.self_order_table_mode = True
+
+ self.env['pos_self_order.custom_link'].create({
+ 'url': '/menu/%s/products' % pos_config_id.id,
+ 'name': 'View Menu',
+ 'pos_config_ids': pos_config_id,
+ 'style': 'primary',
+ })
+
+ return pos_config_ids
@api.depends("module_pos_restaurant")
def _compute_self_order(self):
@@ -86,14 +92,14 @@ class PosConfig(models.Model):
base_route = f"/menu/{self.id}"
if not self.self_order_table_mode:
return base_route
- table_access_token = (
+ access_token = (
self.env["restaurant.table"]
.search(
[("active", "=", True), *(table_id and [("id", "=", table_id)] or [])], limit=1
)
.access_token
)
- return f"{base_route}?at={table_access_token}"
+ return f"{base_route}?at={access_token}"
def _get_self_order_url(self, table_id: Optional[int] = None) -> str:
self.ensure_one()
@@ -165,7 +171,6 @@ class PosConfig(models.Model):
"custom_links": self._get_self_order_custom_links(),
"products": self._get_available_products()._get_self_order_data(self),
"has_active_session": self.has_active_session,
- "orderline_unique_keys": PosOrderLine._get_unique_keys(),
}
def _generate_data_for_qr_codes_page(self, cols: int = 4) -> Dict[str, List[Dict]]:
diff --git a/addons/pos_self_order/models/pos_order.py b/addons/pos_self_order/models/pos_order.py
index 552ce181baf..1ded0c55ea9 100644
--- a/addons/pos_self_order/models/pos_order.py
+++ b/addons/pos_self_order/models/pos_order.py
@@ -4,80 +4,51 @@
import re
from typing import Dict, Union
-from odoo import models
+from odoo import models, fields
class PosOrderLine(models.Model):
_inherit = "pos.order.line"
+ # For the moment we need this to keep attributes consistency between the server and client_side.
+ selected_attributes = fields.Json(string="Selected Attributes")
+
+ # FIXME: uuid already pass in pos and move note in pos_restaurant.
def _export_for_ui(self, orderline):
return {
- "uuid": orderline.uuid,
- "note": orderline.note,
+ 'uuid': orderline.uuid,
+ 'note': orderline.note,
**super()._export_for_ui(orderline),
}
- def _get_description(self):
- """
- The pos sends a "description" key to the backend, which is a string containing the selected value for
- each attribute of the product, separated by a comma. In the db we end up storing the
- "full_product_name", which is composed of the product name and the description.
- :line.full_product_name: ex: "Desk Organizer (M, Leather)"
- :return: ex: "M, Leather"
- """
- self.ensure_one()
- description = re.findall(r"\(([^)]+)\)", self.full_product_name)
- if description:
- # It might happen that the product has a name with parenthesis. ex: "Salad (Vegie)"
- # In that case the full_product_name will be "Salad (Vegie) (Small)", but are interested in returning the variant "Small"
- # That's why we return the last element of the list
- return description[-1]
- return ""
-
- @staticmethod
- def _get_unique_keys():
- """
- These are the keys that define the uniqueness of an orderline.
- We use them to check if 2 orderlines can be merged or not.
- """
- return ["product_id", "description", "customer_note"]
-
class PosOrder(models.Model):
_inherit = "pos.order"
def _export_for_self_order(self) -> Dict:
- """
- Given an order, it returns a dictionary with the keys that we need in the frontend
- """
self.ensure_one()
+
return {
+ "id": self.id,
+ "pos_config_id": self.config_id.id,
"pos_reference": self.pos_reference,
"access_token": self.access_token,
"state": self.state,
"date": str(self.date_order),
"amount_total": self.amount_total,
"amount_tax": self.amount_tax,
- "items": [
+ "lines": [
{
+ "id": line.id,
+ "price_subtotal": line.price_subtotal,
+ "price_subtotal_incl": line.price_subtotal_incl,
"product_id": line.product_id.id,
+ "selected_attributes": line.selected_attributes,
+ "uuid": line.uuid,
"qty": line.qty,
"customer_note": line.customer_note,
- "price_extra": line.product_id._get_price_info(
- self.config_id,
- line.price_extra,
- ),
- "description": line._get_description(),
+ "full_product_name": line.full_product_name,
}
for line in self.lines
],
}
-
- def _get_self_order_data(self) -> Dict[str, Union[str, int]]:
- return {
- "id": self.pos_reference,
- "sequence_number": self.sequence_number,
- "access_token": self.access_token,
- "session_id": self.session_id.id,
- "table_id": self.table_id.id,
- }
diff --git a/addons/pos_self_order/models/pos_restaurant.py b/addons/pos_self_order/models/pos_restaurant.py
index 8f21f0fd1a1..a3adcbe03b7 100644
--- a/addons/pos_self_order/models/pos_restaurant.py
+++ b/addons/pos_self_order/models/pos_restaurant.py
@@ -42,6 +42,7 @@ class RestaurantTable(models.Model):
def _get_data_for_qr_codes_page(self, url: Callable[[Optional[int]], str]) -> List[Dict]:
return [
{
+ "access_token": table.access_token,
"id": table.id,
"name": table.name,
"url": url(table.id),
diff --git a/addons/pos_self_order/models/product_product.py b/addons/pos_self_order/models/product_product.py
index e1cf42aaefb..781374cca29 100644
--- a/addons/pos_self_order/models/product_product.py
+++ b/addons/pos_self_order/models/product_product.py
@@ -62,6 +62,7 @@ class ProductProduct(models.Model):
)
return attributes
+ # FIXME: this method should be verified about price computation (pricelist taxes....)
def _get_price_info(
self, pos_config: PosConfig, price: Optional[float] = None, qty: int = 1
) -> Dict[str, float]:
diff --git a/addons/pos_self_order/models/res_config_settings.py b/addons/pos_self_order/models/res_config_settings.py
index 713bde19d8b..806b683e8c4 100644
--- a/addons/pos_self_order/models/res_config_settings.py
+++ b/addons/pos_self_order/models/res_config_settings.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
-from odoo import models, fields, api
+from odoo import models, fields, api, _
+from odoo.exceptions import UserError
class ResConfigSettings(models.TransientModel):
@@ -70,6 +71,9 @@ class ResConfigSettings(models.TransientModel):
"""
Generate the data needed to print the QR codes page
"""
+ if len(self.pos_config_id.floor_ids.table_ids) == 0:
+ raise UserError(_("Before you can create QR codes, you need to create tables for the restaurant."))
+
return self.env.ref("pos_self_order.report_self_order_qr_codes_page").report_action(
[], data=self.pos_config_id._generate_data_for_qr_codes_page(cols=3)
)
diff --git a/addons/pos_self_order/static/src/Components/IncrementCounter/IncrementCounter.js b/addons/pos_self_order/static/src/Components/IncrementCounter/IncrementCounter.js
deleted file mode 100644
index a1a7af3d3ec..00000000000
--- a/addons/pos_self_order/static/src/Components/IncrementCounter/IncrementCounter.js
+++ /dev/null
@@ -1,8 +0,0 @@
-/** @odoo-module */
-
-import { Component } from "@odoo/owl";
-
-export class IncrementCounter extends Component {
- static template = "pos_self_order.IncrementCounter";
- static props = ["value", "onClick"];
-}
diff --git a/addons/pos_self_order/static/src/Components/IncrementCounter/IncrementCounter.scss b/addons/pos_self_order/static/src/Components/IncrementCounter/IncrementCounter.scss
deleted file mode 100644
index 1593cdecea1..00000000000
--- a/addons/pos_self_order/static/src/Components/IncrementCounter/IncrementCounter.scss
+++ /dev/null
@@ -1,4 +0,0 @@
-.o_self_order_incr_button button {
- width: 40px;
- height: 40px;
-}
\ No newline at end of file
diff --git a/addons/pos_self_order/static/src/Components/IncrementCounter/IncrementCounter.xml b/addons/pos_self_order/static/src/Components/IncrementCounter/IncrementCounter.xml
deleted file mode 100644
index 61f24267e8c..00000000000
--- a/addons/pos_self_order/static/src/Components/IncrementCounter/IncrementCounter.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-