Browse Source

Aug 05: [FIX] Bug Fixed 'pos_kitchen_screen_odoo'

18.0
Risvana Cybro 5 days ago
parent
commit
a3fa247421
  1. 3
      pos_kitchen_screen_odoo/__manifest__.py
  2. 7
      pos_kitchen_screen_odoo/doc/RELEASE_NOTES.md
  3. 1
      pos_kitchen_screen_odoo/models/__init__.py
  4. 6
      pos_kitchen_screen_odoo/models/kitchen_screen.py
  5. 513
      pos_kitchen_screen_odoo/models/pos_orders.py
  6. 4
      pos_kitchen_screen_odoo/models/pos_session.py
  7. 26
      pos_kitchen_screen_odoo/models/product_product.py
  8. 35
      pos_kitchen_screen_odoo/static/description/index.html
  9. 378
      pos_kitchen_screen_odoo/static/src/js/kitchen_screen.js
  10. 41
      pos_kitchen_screen_odoo/static/src/js/order_button.js
  11. 94
      pos_kitchen_screen_odoo/static/src/xml/kitchen_screen_templates.xml
  12. 15
      pos_kitchen_screen_odoo/views/kitchen_screen_views.xml
  13. 13
      pos_kitchen_screen_odoo/views/product_product_views.xml

3
pos_kitchen_screen_odoo/__manifest__.py

@ -21,7 +21,7 @@
############################################################################
{
'name': 'POS Kitchen Screen',
'version': '18.0.1.1.2',
'version': '18.0.1.1.3',
'category': 'Point Of Sale',
'summary': 'POS Kitchen Screen facilitates sending certain orders '
'automatically to the kitchen.The POS Kitchen Screen allows for '
@ -51,6 +51,7 @@
"views/kitchen_screen_views.xml",
"views/pos_kitchen_screen_odoo_menus.xml",
"views/pos_order_views.xml",
"views/product_product_views.xml",
],
'assets': {
'point_of_sale._assets_pos': [

7
pos_kitchen_screen_odoo/doc/RELEASE_NOTES.md

@ -22,3 +22,10 @@
#### Version 18.0.1.1.2
#### BUG FIX
- Fixed issue where order reference in order list become mistmatch/wrong, get order ref from other POS.
#### 25.07.2025
#### Version 18.0.1.1.3
#### BUG FIX
- Fixed issue where latest orders disappearing from kitchen screen when plan button is clicked from POS.
- Fixed issue where orders in kitchen screen required manual refresh.
- Fixed issue when adding items to the order and quantity defaults to one.

1
pos_kitchen_screen_odoo/models/__init__.py

@ -22,3 +22,4 @@
from . import kitchen_screen
from . import pos_orders
from . import pos_session
from . import product_product

6
pos_kitchen_screen_odoo/models/kitchen_screen.py

@ -50,6 +50,12 @@ class KitchenScreen(models.Model):
shop_number = fields.Integer(related='pos_config_id.id', string='Customer',
help="Id of the POS")
is_preparation_complete = fields.Boolean(
string='Change Stage',
default=False,
help='Change the cooking stage when completing the preparation time',
)
def kitchen_screen(self):
"""Redirect to corresponding kitchen screen for the cook"""
return {

513
pos_kitchen_screen_odoo/models/pos_orders.py

@ -25,108 +25,168 @@ import pytz
class PosOrder(models.Model):
"""Inheriting the pos order model """
_inherit = "pos.order"
order_status = fields.Selection(string="Order Status",
selection=[("draft", "Draft"),
("waiting", "Cooking"),
("ready", "Ready"),
("cancel", "Cancel")],
selection=[("draft", "Cooking Orders"),
("waiting", "Ready Orders"),
("ready", "Completed Orders"),
("cancel", "Cancelled Orders")],
default='draft',
help='To know the status of order')
help='Kitchen workflow status: draft=cooking, waiting=ready, ready=completed')
order_ref = fields.Char(string="Order Reference",
help='Reference of the order')
is_cooking = fields.Boolean(string="Is Cooking",
help='To identify the order is kitchen orders')
help='To identify the order is kitchen orders')
hour = fields.Char(string="Order Time", readonly=True,
help='To set the time of each order')
minutes = fields.Char(string='order time')
floor = fields.Char(string='Floor time')
avg_prepare_time = fields.Float(string="Avg Prepare Time", store=True)
def write(self, vals):
"""Super the write function for adding order status in vals"""
if vals.get("state") == "paid" and "order_status" in vals:
vals.pop("order_status")
message = {
'res_model': self._name,
'message': 'pos_order_created'
}
self.env["bus.bus"]._sendone('pos_order_created',
"notification",
message)
for order in self:
if order.order_status == "waiting" and vals.get(
"order_status") != "ready":
vals["order_status"] = order.order_status
if vals.get("state") and vals[
"state"] == "paid" and order.name == "/":
vals["name"] = self._compute_order_name()
return super(PosOrder, self).write(vals)
@api.model_create_multi
def create(self, vals_list):
"""Override create function for the validation of the order"""
message = {
'res_model': self._name,
'message': 'pos_order_created'
}
self.env["bus.bus"]._sendone('pos_order_created',
"notification",
message)
processed_vals_to_create = []
for vals in vals_list:
if not vals["order_status"]:
product_ids = [item[2]['product_id'] for item in vals.get('lines')]
if product_ids:
prepare_times = self.env['product.product'].search([('id', 'in', product_ids)]).mapped(
'prepair_time_minutes')
vals['avg_prepare_time'] = max(prepare_times)
existing_order = self.search([("pos_reference", "=", vals.get("pos_reference"))], limit=1)
if existing_order:
continue
if not vals.get("order_status"):
vals["order_status"] = 'draft'
pos_orders = self.search(
[("pos_reference", "=", vals["pos_reference"])])
if pos_orders:
return super().create(vals_list)
else:
if vals.get('order_id') and not vals.get('name'):
# set name based on the sequence specified on the config
config = self.env['pos.order'].browse(vals['order_id']).session_id.config_id
if config and config.sequence_line_id:
vals['name'] = config.sequence_line_id._next()
else:
# Generate a unique name using a default fallback sequence
vals['name'] = self.env['ir.sequence'].next_by_code('pos.order')
return super().create(vals_list)
def get_details(self, shop_id, order=None):
"""For getting the kitchen orders for the cook"""
dic = order
if order:
orders = self.search(
[("pos_reference", "=", order[0]['pos_reference'])])
if not orders:
self.create(dic)
else:
orders.floor = dic[0]['floor']
orders.hour = dic[0]['hour']
orders.minutes = dic[0]['minutes']
orders.lines.write({'is_cooking': True})
if vals.get('order_id') and not vals.get('name'):
config = self.env['pos.order'].browse(vals['order_id']).session_id.config_id
if config.sequence_line_id:
vals['name'] = config.sequence_line_id._next()
elif not vals.get('name'):
vals['name'] = self.env['ir.sequence'].next_by_code('pos.order')
processed_vals_to_create.append(vals)
res = super().create(processed_vals_to_create) if processed_vals_to_create else self.browse()
orders_to_notify = []
for order in res:
kitchen_screen = self.env["kitchen.screen"].search(
[("pos_config_id", "=", order.config_id.id)], limit=1
)
if kitchen_screen:
has_kitchen_items = False
for order_line in order.lines:
if order_line.product_id.pos_categ_ids and any(
cat.id in kitchen_screen.pos_categ_ids.ids for cat in order_line.product_id.pos_categ_ids):
order_line.is_cooking = True
has_kitchen_items = True
if has_kitchen_items:
order.is_cooking = True
order.order_ref = order.name
if order.order_status != 'draft':
order.order_status = 'draft'
orders_to_notify.append(order)
self.env.cr.commit()
for order in orders_to_notify:
message = {
'res_model': self._name,
'message': 'pos_order_created',
'order_id': order.id,
'config_id': order.config_id.id
}
channel = f'pos_order_created_{order.config_id.id}'
self.env["bus.bus"]._sendone(channel, "notification", message)
return res
def write(self, vals):
"""Override write function for adding order status in vals"""
original_statuses = {order.id: order.order_status for order in self}
res = super(PosOrder, self).write(vals)
for order in self:
kitchen_screen = self.env["kitchen.screen"].search(
[("pos_config_id", "=", order.config_id.id)], limit=1
)
if kitchen_screen:
has_kitchen_items = False
for line_data in order.lines:
if line_data.product_id.pos_categ_ids and any(
cat.id in kitchen_screen.pos_categ_ids.ids for cat in line_data.product_id.pos_categ_ids):
has_kitchen_items = True
break
if has_kitchen_items and not order.is_cooking:
order.is_cooking = True
if order.order_status not in ['waiting', 'ready', 'cancel']:
order.order_status = 'draft'
elif not has_kitchen_items and order.is_cooking:
order.is_cooking = False
if order.is_cooking and order.id in original_statuses:
if original_statuses[order.id] != order.order_status:
message = {
'res_model': self._name,
'message': 'pos_order_updated',
'order_id': order.id,
'config_id': order.config_id.id,
'new_status': order.order_status
}
channel = f'pos_order_created_{order.config_id.id}'
self.env["bus.bus"]._sendone(channel, "notification", message)
return res
@api.model
def get_details(self, shop_id, *args, **kwargs):
"""Method to fetch kitchen orders for display on the kitchen screen."""
kitchen_screen = self.env["kitchen.screen"].sudo().search(
[("pos_config_id", "=", shop_id)])
pos_orders = self.env["pos.order.line"].search(
["&", ("is_cooking", "=", True),
("product_id.pos_categ_ids", "in",
[rec.id for rec in kitchen_screen.pos_categ_ids])])
pos = self.env["pos.order"].search(
[("lines", "in", [rec.id for rec in pos_orders])],
order="date_order")
pos_lines = pos.lines.search(
[("product_id.pos_categ_ids", "in",
[rec.id for rec in kitchen_screen.pos_categ_ids])])
values = {"orders": pos.read(), "order_lines": pos_lines.read()}
if not kitchen_screen:
return {"orders": [], "order_lines": []}
pos_orders = self.env["pos.order"].search([
("is_cooking", "=", True),
("config_id", "=", shop_id),
("state", "!=", "cancel"),
("order_status", "!=", "cancel"),
"|", "|",
("order_status", "=", "draft"),
("order_status", "=", "waiting"),
("order_status", "=", "ready")
], order="date_order")
pos_lines = pos_orders.lines.filtered(
lambda line: line.is_cooking and any(
categ.id in kitchen_screen.pos_categ_ids.ids
for categ in line.product_id.pos_categ_ids
)
)
values = {"orders": pos_orders.read(), "order_lines": pos_lines.read()}
user_tz_str = self.env.user.tz or 'UTC'
user_tz = pytz.timezone(user_tz_str)
utc = pytz.utc
for value in values['orders']:
if value.get('table_id'):
value['floor'] = value['table_id'][1].split(',')[0].strip()
date_str = value['date_order']
date_str = value['date_order']
try:
if isinstance(date_str, str):
utc_dt = datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S")
utc_dt = utc.localize(utc_dt)
@ -135,122 +195,277 @@ class PosOrder(models.Model):
local_dt = utc_dt.astimezone(user_tz)
value['hour'] = local_dt.hour
value['formatted_minutes'] = f"{local_dt.minute:02d}"
value['minutes'] = local_dt.minute
except Exception:
value['hour'] = 0
value['minutes'] = 0
value['formatted_minutes'] = "00"
return values
def action_pos_order_paid(self):
"""Supering the action_pos_order_paid function for setting its kitchen
order and setting the order reference"""
"""Inherited method called when a POS order transitions to 'paid' state."""
res = super().action_pos_order_paid()
kitchen_screen = self.env["kitchen.screen"].search(
[("pos_config_id", "=", self.config_id.id)]
[("pos_config_id", "=", self.config_id.id)], limit=1
)
for order_line in self.lines:
order_line.is_cooking = True
if kitchen_screen:
for line in self.lines:
line.is_cooking = True
self.is_cooking = True
self.order_ref = self.name
has_kitchen_items = False
for order_line in self.lines:
if order_line.product_id.pos_categ_ids and any(
cat.id in kitchen_screen.pos_categ_ids.ids for cat in order_line.product_id.pos_categ_ids):
order_line.is_cooking = True
has_kitchen_items = True
if has_kitchen_items:
self.is_cooking = True
self.order_ref = self.name
if not self.order_status or self.order_status == 'draft':
self.order_status = 'draft'
message = {
'res_model': self._name,
'message': 'pos_order_created',
'order_id': self.id,
'config_id': self.config_id.id
}
channel = f'pos_order_created_{self.config_id.id}'
self.env["bus.bus"]._sendone(channel, "notification", message)
return res
@api.onchange("order_status")
def _onchange_is_cooking(self):
"""To set is_cooking false"""
"""Automatically unmark as 'cooking' when order status becomes 'ready'."""
if self.order_status == "ready":
self.is_cooking = False
def order_progress_draft(self):
"""Calling function from js to change the order status"""
"""Action for "Accept" button: Move order from 'draft' (cooking) to 'waiting' (ready) status."""
self.ensure_one()
old_status = self.order_status
self.order_status = "waiting"
for line in self.lines:
if line.order_status != "ready":
if line.order_status not in ["ready", "cancel"]:
line.order_status = "waiting"
if old_status != "waiting":
message = {
'res_model': self._name,
'message': 'pos_order_accepted',
'order_id': self.id,
'config_id': self.config_id.id
}
channel = f'pos_order_created_{self.config_id.id}'
self.env["bus.bus"]._sendone(channel, "notification", message)
def order_progress_cancel(self):
"""Calling function from js to change the order status"""
"""Action for "Cancel" button: Move order to 'cancel' status."""
self.ensure_one()
self.order_status = "cancel"
for line in self.lines:
if line.order_status != "ready":
line.order_status = "cancel"
line.order_status = "cancel"
message = {
'res_model': self._name,
'message': 'pos_order_cancelled',
'order_id': self.id,
'config_id': self.config_id.id
}
channel = f'pos_order_created_{self.config_id.id}'
self.env["bus.bus"]._sendone(channel, "notification", message)
def order_progress_change(self):
"""Calling function from js to change the order status"""
"""Action for "Done" button: Move order from 'waiting' (ready) to 'ready' (completed) status."""
self.ensure_one()
self.order_status = "ready"
kitchen_screen = self.env["kitchen.screen"].search(
[("pos_config_id", "=", self.config_id.id)])
stage = []
for line in self.lines:
for categ in line.product_id.pos_categ_ids:
if categ.id in [rec.id for rec in
kitchen_screen.pos_categ_ids]:
stage.append(line.order_status)
if "waiting" in stage or "draft" in stage:
self.order_status = "ready"
else:
self.order_status = "ready"
[("pos_config_id", "=", self.config_id.id)], limit=1)
if kitchen_screen:
for line in self.lines:
if line.product_id.pos_categ_ids and any(
cat.id in kitchen_screen.pos_categ_ids.ids for cat in line.product_id.pos_categ_ids):
line.order_status = "ready"
message = {
'res_model': self._name,
'message': 'pos_order_completed',
'order_id': self.id,
'config_id': self.config_id.id
}
channel = f'pos_order_created_{self.config_id.id}'
self.env["bus.bus"]._sendone(channel, "notification", message)
@api.model
def check_order(self, order_name):
"""Calling function from js to know status of the order"""
"""Check if an order exists, has kitchen items, and is not yet completed/cancelled."""
pos_order = self.env['pos.order'].sudo().search(
[('pos_reference', '=', str(order_name))])
kitchen_order = self.env['kitchen.screen'].sudo().search(
[('pos_config_id', '=', pos_order.config_id.id)])
if kitchen_order:
for category in pos_order.lines.mapped('product_id').mapped(
'pos_categ_ids').mapped('id'):
if category not in kitchen_order.pos_categ_ids.mapped('id'):
return {
'category': pos_order.lines.product_id.pos_categ_ids.browse(
category).name}
if kitchen_order and pos_order:
if pos_order.order_status != 'ready':
return True
else:
return False
[('pos_reference', '=', str(order_name))], limit=1)
if not pos_order:
return False
kitchen_screen = self.env['kitchen.screen'].sudo().search(
[("pos_config_id", "=", pos_order.config_id.id)], limit=1)
if not kitchen_screen:
return False
unhandled_categories = []
for line in pos_order.lines:
if line.product_id.pos_categ_ids and not any(
cat.id in kitchen_screen.pos_categ_ids.ids for cat in line.product_id.pos_categ_ids):
unhandled_categories.extend(
[c.name for c in line.product_id.pos_categ_ids if c.id not in kitchen_screen.pos_categ_ids.ids])
if unhandled_categories:
return {'category': ", ".join(list(set(unhandled_categories)))}
if pos_order.order_status not in ['ready', 'cancel']:
return True
else:
return False
def check_order_status(self, order_name):
"Update order status"
pos_order = self.env['pos.order'].sudo().search(
[('pos_reference', '=', str(order_name))])
kitchen_order = self.env['kitchen.screen'].sudo().search(
[('pos_config_id', '=', pos_order.config_id.id)])
for category in pos_order.lines.mapped('product_id').mapped(
'pos_categ_ids').mapped('id'):
if category not in kitchen_order.pos_categ_ids.mapped('id'):
return 'no category'
if kitchen_order:
if pos_order.order_status == 'ready':
return False
@api.model
def create_or_update_kitchen_order(self, orders_data):
"""Create new kitchen order or update existing one with new items."""
for order_data in orders_data:
pos_reference = order_data.get('pos_reference')
existing_order = self.search([('pos_reference', '=', pos_reference)], limit=1)
kitchen_screen = self.env["kitchen.screen"].search([
("pos_config_id", "=", order_data.get('config_id'))
], limit=1)
if not kitchen_screen:
continue
if existing_order:
if existing_order.order_status in ['ready', 'cancel']:
continue
existing_line_products = {line.product_id.id: line for line in existing_order.lines}
for line_data in order_data.get('lines', []):
line_vals = line_data[2]
product_id = line_vals.get('product_id')
qty = line_vals.get('qty', 1)
product = self.env['product.product'].browse(product_id)
if not (product.pos_categ_ids and any(
cat.id in kitchen_screen.pos_categ_ids.ids
for cat in product.pos_categ_ids
)):
continue
if product_id in existing_line_products:
existing_line = existing_line_products[product_id]
if existing_line.qty != qty:
existing_line.write({'qty': qty})
else:
self.env['pos.order.line'].create({
'order_id': existing_order.id,
'product_id': product_id,
'qty': qty,
'price_unit': line_vals.get('price_unit'),
'price_subtotal': line_vals.get('price_subtotal'),
'price_subtotal_incl': line_vals.get('price_subtotal_incl'),
'discount': line_vals.get('discount', 0),
'full_product_name': line_vals.get('full_product_name'),
'is_cooking': True,
'order_status': 'draft',
'note': line_vals.get('note', ''),
})
existing_order.write({
'amount_total': order_data.get('amount_total'),
'amount_tax': order_data.get('amount_tax'),
})
else:
return True
else:
kitchen_lines = []
for line_data in order_data.get('lines', []):
line_vals = line_data[2]
product_id = line_vals.get('product_id')
product = self.env['product.product'].browse(product_id)
if product.pos_categ_ids and any(
cat.id in kitchen_screen.pos_categ_ids.ids
for cat in product.pos_categ_ids
):
kitchen_lines.append([0, 0, {
'product_id': product_id,
'qty': line_vals.get('qty', 1),
'price_unit': line_vals.get('price_unit'),
'price_subtotal': line_vals.get('price_subtotal'),
'price_subtotal_incl': line_vals.get('price_subtotal_incl'),
'discount': line_vals.get('discount', 0),
'full_product_name': line_vals.get('full_product_name'),
'is_cooking': True,
'order_status': 'draft',
'note': line_vals.get('note', ''),
}])
if kitchen_lines:
self.create({
'pos_reference': pos_reference,
'session_id': order_data.get('session_id'),
'amount_total': order_data.get('amount_total'),
'amount_paid': order_data.get('amount_paid', 0),
'amount_return': order_data.get('amount_return', 0),
'amount_tax': order_data.get('amount_tax'),
'lines': kitchen_lines,
'is_cooking': True,
'order_status': 'draft',
'company_id': order_data.get('company_id'),
'table_id': order_data.get('table_id'),
'config_id': order_data.get('config_id'),
'state': 'draft',
'name': self.env['ir.sequence'].next_by_code('pos.order') or '/',
})
message = {
'res_model': 'pos.order',
'message': 'pos_order_updated',
'config_id': order_data.get('config_id')
}
channel = f'pos_order_created_{order_data.get("config_id")}'
self.env["bus.bus"]._sendone(channel, "notification", message)
return True
@api.model
def check_order_status(self, dummy_param, order_reference):
"""Check if items can be added to an order based on its status."""
pos_order = self.env['pos.order'].sudo().search([
('pos_reference', '=', str(order_reference))
], limit=1)
if not pos_order:
return True
if pos_order.order_status in ['draft', 'waiting']:
return True
else:
return False
class PosOrderLine(models.Model):
"""Inheriting the pos order line"""
_inherit = "pos.order.line"
order_status = fields.Selection(
selection=[('draft', 'Draft'), ('waiting', 'Cooking'),
('ready', 'Ready'), ('cancel', 'Cancel')], default='draft',
help='The status of orderliness')
selection=[('draft', 'Cooking'), ('waiting', 'Ready'),
('ready', 'Completed'), ('cancel', 'Cancel')], default='draft',
help='Kitchen workflow status: draft=cooking, waiting=ready, ready=completed')
order_ref = fields.Char(related='order_id.order_ref',
string='Order Reference',
help='Order reference of order')
is_cooking = fields.Boolean(string="Cooking", default=False,
help='To identify the order is '
'kitchen orders')
help='To identify the order is kitchen orders')
customer_id = fields.Many2one('res.partner', string="Customer",
related='order_id.partner_id',
help='Id of the customer')
def get_product_details(self, ids):
"""To get the product details"""
lines = self.env['pos.order'].browse(ids)
"""Fetch details for specific order lines."""
lines = self.env['pos.order.line'].browse(ids)
res = []
for rec in lines:
res.append({
@ -261,8 +476,22 @@ class PosOrderLine(models.Model):
return res
def order_progress_change(self):
"""Calling function from js to change the order_line status"""
"""Toggle status of an order line between 'waiting' and 'ready'."""
self.ensure_one()
old_status = self.order_status
if self.order_status == 'ready':
self.order_status = 'waiting'
else:
self.order_status = 'ready'
if old_status != self.order_status:
message = {
'res_model': 'pos.order.line',
'message': 'pos_order_line_updated',
'line_id': self.id,
'order_id': self.order_id.id,
'config_id': self.order_id.config_id.id,
'new_status': self.order_status
}
channel = f'pos_order_created_{self.order_id.config_id.id}'
self.env["bus.bus"]._sendone(channel, "notification", message)

4
pos_kitchen_screen_odoo/models/pos_session.py

@ -30,8 +30,8 @@ class PosSession(models.Model):
"""Pos ui models to load"""
result = super()._pos_ui_models_to_load()
result += {
'pos.order', 'pos.order.line'
}
'pos.order', 'pos.order.line'
}
return result
def _loader_params_pos_order(self):

26
pos_kitchen_screen_odoo/models/product_product.py

@ -0,0 +1,26 @@
from odoo import models, fields, api
from odoo.exceptions import ValidationError
import re
class ProductProduct(models.Model):
_inherit = 'product.product'
prepair_time_minutes = fields.Float(
string='Preparation Time (MM:SS)',
digits=(12, 2),
help="Enter time in MM:SS format (e.g., 20:12 for 20 minutes 12 seconds)"
)
@api.onchange('prepair_time_minutes')
def _onchange_prepair_time(self):
if isinstance(self.prepair_time_minutes, str):
try:
# Validate format MM:SS
if not re.match(r'^\d{1,3}:[0-5][0-9]$', self.prepair_time_minutes):
raise ValidationError("Please enter time in MM:SS format (e.g., 20:12)")
minutes, seconds = map(int, self.prepair_time_minutes.split(':'))
self.prepair_time_minutes = minutes + (seconds / 60.0)
except (ValueError, AttributeError):
raise ValidationError("Invalid time format. Please use MM:SS (e.g., 20:12)")

35
pos_kitchen_screen_odoo/static/description/index.html

@ -960,6 +960,41 @@
</div>
</div>
</div>
<div class="row pt-5 m-0">
<div class="col-md-3">
<h4 style="font-size:16px; font-weight:600; color:#514F4F; margin:0; line-height:26px;">
Latest Release 18.0.1.1.3
</h4>
<span style="font-size:14px; color:#7A7979; display:block; margin-bottom:20px;">
25th July, 2025
</span>
</div>
<div class="col-md-8">
<div style="padding:0 0 40px">
<div style="margin:0 0 10px">
<div style="display:inline-block; padding:0px 8px; color:#514F4F; background-color:#FFD8D8; border-radius:20px">
Bug Fix
</div>
</div>
<div class="d-flex m-0"
style="color:#7A7979;">
<ul class="pl-3 mb-0">
<li>
Fixed issue where latest orders disappearing from kitchen screen when plan button is clicked from POS.
</li>
<li>
Fixed issue where orders in kitchen screen required manual refresh.
</li>
<li>
Fixed issue when adding items to the order and quantity defaults to one.
</li>
</ul>
</div>
</div>
<div style="padding:0 0 0; border-bottom:1px solid #E3E3E3">
</div>
</div>
</div>
</div>
</div>
</div>

378
pos_kitchen_screen_odoo/static/src/js/kitchen_screen.js

@ -1,144 +1,304 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
const { Component, onWillStart, useState, onMounted } = owl;
const { Component, onMounted, onWillUnmount, useState } = owl;
import { useService } from "@web/core/utils/hooks";
class kitchen_screen_dashboard extends Component {
setup(env) {
class KitchenScreenDashboard extends Component {
setup() {
super.setup();
this.busService = this.env.services.bus_service;
this.busService.addChannel("pos_order_created");
onWillStart(() => {
this.busService.subscribe('notification', this.onPosOrderCreation.bind(this));})
// Services
this.action = useService("action");
this.rpc = this.env.services.rpc;
this.action = useService("action");
this.orm = useService("orm");
var self=this
this.busService = useService("bus_service");
// Method binding
this.getCurrentShopId = this.getCurrentShopId.bind(this);
this.loadOrders = this.loadOrders.bind(this);
this.startCountdown = this.startCountdown.bind(this);
this.updateCountdownState = this.updateCountdownState.bind(this);
this.onPosOrderCreation = this.onPosOrderCreation.bind(this);
this.accept_order = this.accept_order.bind(this);
this.done_order = this.done_order.bind(this);
this.cancel_order = this.cancel_order.bind(this);
this.accept_order_line = this.accept_order_line.bind(this);
this.forceRefresh = this.forceRefresh.bind(this);
// Stage change methods
this.ready_stage = (e) => this.state.stages = 'ready';
this.waiting_stage = (e) => this.state.stages = 'waiting';
this.draft_stage = (e) => this.state.stages = 'draft';
// Initialization
this.currentShopId = this.getCurrentShopId();
this.channel = `pos_order_created_${this.currentShopId}`;
this.countdownIntervals = {};
// State management
this.state = useState({
order_details: [],
shop_id:[],
shop_id: this.currentShopId,
stages: 'draft',
draft_count:[],
waiting_count:[],
ready_count:[],
lines:[]
draft_count: 0,
waiting_count: 0,
ready_count: 0,
lines: [],
prepare_times: [],
countdowns: {},
isLoading: false
});
// Component lifecycle
onMounted(() => {
this.busService.addChannel(this.channel);
this.busService.subscribe('notification', this.onPosOrderCreation);
this.loadOrders();
this.autoRefreshInterval = setInterval(() => {
this.loadOrders();
}, 30000);
});
this.orm.call("pos.session", "search_read", [[
["state", "=", "opened"] // Get only open sessions
]]).then(function(sessions) {
if (sessions.length > 0) {
self.state.session_ids = sessions.map(session => session.id); // Store session IDs in state
} else {
self.state.session_ids = []
}
onWillUnmount(() => {
this.busService.deleteChannel(this.channel);
this.busService.unsubscribe('notification', this.onPosOrderCreation);
if (this.autoRefreshInterval) {
clearInterval(this.autoRefreshInterval);
}
Object.values(this.countdownIntervals).forEach(interval => {
clearInterval(interval);
});
this.countdownIntervals = {};
});
}
var session_shop_id;
// //if refreshing the page then the last passed context (shop id)
// //save to the session storage
if (this.props.action.context.default_shop_id) {
getCurrentShopId() {
let session_shop_id;
if (this.props.action?.context?.default_shop_id) {
sessionStorage.setItem('shop_id', this.props.action.context.default_shop_id);
this.shop_id = this.props.action.context.default_shop_id;
session_shop_id = sessionStorage.getItem('shop_id');
session_shop_id = this.props.action.context.default_shop_id;
} else {
session_shop_id = sessionStorage.getItem('shop_id');
this.shop_id = parseInt(session_shop_id, 10);;
}
self.orm.call("pos.order", "get_details", ["", self.shop_id,""]).then(function(result) {
self.state.order_details = result['orders'].filter(order => order.session_id && self.state.session_ids.includes(order.session_id[0]));
self.state.lines = result['order_lines']
self.state.shop_id=self.shop_id
self.state.draft_count=self.state.order_details.filter((order) => order.order_status=='draft' && order.config_id[0]==self.state.shop_id).length
self.state.waiting_count=self.state.order_details.filter((order) => order.order_status=='waiting' && order.config_id[0]==self.state.shop_id).length
self.state.ready_count=self.state.order_details.filter((order) => order.order_status=='ready' && order.config_id[0]==self.state.shop_id).length
});
return parseInt(session_shop_id, 10) || 0;
}
//Calling the onPosOrderCreation when an order is created or edited on the backend and return the notification
onPosOrderCreation(message){
var self=this
if(message.message == "pos_order_created" && message.res_model == "pos.order"){
self.orm.call("pos.order", "get_details", ["", self.shop_id,""]).then(function(result) {
self.state.order_details = result['orders'].filter(order => order.session_id && self.state.session_ids.includes(order.session_id[0]));
self.state.lines = result['order_lines']
self.state.shop_id=self.shop_id
self.state.draft_count=self.state.order_details.filter((order) => order.order_status=='draft' && order.config_id[0]==self.state.shop_id).length
self.state.waiting_count=self.state.order_details.filter((order) => order.order_status=='waiting' && order.config_id[0]==self.state.shop_id).length
self.state.ready_count=self.state.order_details.filter((order) => order.order_status=='ready' && order.config_id[0]==self.state.shop_id).length
async loadOrders() {
if (this.state.isLoading) return;
try {
this.state.isLoading = true;
const result = await this.orm.call("pos.order", "get_details", [this.currentShopId]);
this.state.order_details = result.orders || [];
this.state.lines = result.order_lines || [];
const activeOrders = this.state.order_details.filter(order => {
const configMatch = Array.isArray(order.config_id) ?
order.config_id[0] === this.currentShopId :
order.config_id === this.currentShopId;
return configMatch && order.order_status !== 'cancel' && order.state !== 'cancel';
});
const productIds = [...new Set(this.state.lines.map(line => line.product_id[0]))];
if (productIds.length) {
const overTimes = await this.orm.call(
"product.product",
"search_read",
[[["id", "in", productIds]], ["id", "prepair_time_minutes"]]
);
this.state.prepare_times = overTimes.map(item => ({
...item,
prepare_time: !item.prepair_time_minutes ? "00:00:00" :
typeof item.prepair_time_minutes === 'number' ?
parseFloat(item.prepair_time_minutes.toFixed(2)) :
item.prepair_time_minutes
}));
}
this.state.draft_count = activeOrders.filter(o => o.order_status === 'draft').length;
this.state.waiting_count = activeOrders.filter(o => o.order_status === 'waiting').length;
this.state.ready_count = activeOrders.filter(o => o.order_status === 'ready').length;
activeOrders.forEach(order => {
if (order.order_status === 'waiting' && order.avg_prepare_time) {
if (!this.countdownIntervals[order.id]) {
this.startCountdown(order.id, order.avg_prepare_time);
}
} else if (order.order_status === 'ready') {
this.updateCountdownState(order.id, 0, true);
if (this.countdownIntervals[order.id]) {
clearInterval(this.countdownIntervals[order.id]);
delete this.countdownIntervals[order.id];
}
}
});
} catch (error) {
console.error("Error loading orders:", error);
} finally {
this.state.isLoading = false;
}
}
// cancel the order from the kitchen
cancel_order(e) {
var input_id = $("#" + e.target.id).val();
this.orm.call("pos.order", "order_progress_cancel", [Number(input_id)])
var current_order = this.state.order_details.filter((order) => order.id==input_id)
if(current_order){
current_order[0].order_status = 'cancel'
}
}
// accept the order from the kitchen
accept_order(e) {
var input_id = $("#" + e.target.id).val();
ScrollReveal().reveal("#" + e.target.id, {
delay: 1000,
duration: 2000,
opacity: 0,
distance: "50%",
origin: "top",
reset: true,
interval: 600,
});
var self=this
this.orm.call("pos.order", "order_progress_draft", [Number(input_id)])
var current_order = this.state.order_details.filter((order) => order.id==input_id)
if(current_order){
current_order[0].order_status = 'waiting'
}
async startCountdown(orderId, timeString,config_id) {
if (this.countdownIntervals[orderId]) {
clearInterval(this.countdownIntervals[orderId]);
}
const [minutes, seconds] = timeString.toFixed(2).split('.').map(Number);
let totalSeconds = minutes * 60 + seconds;
this.updateCountdownState(orderId, totalSeconds, false);
this.countdownIntervals[orderId] = setInterval(async () => {
totalSeconds--;
this.updateCountdownState(orderId, totalSeconds, false);
if (totalSeconds <= 0) {
try {
let orderData = await this.orm.call(
'kitchen.screen',
'search_read',
[
[["pos_config_id", "=", config_id[0]]],
["is_preparation_complete"]
]
);
clearInterval(this.countdownIntervals[orderId]);
delete this.countdownIntervals[orderId];
this.updateCountdownState(orderId, 0, true);
if (orderData[0].is_preparation_complete === true){
this.done_order({ target: { value: orderId.toString() } });
}
} catch (error) {
console.error("Error fetching order data:", error);
// Handle error appropriately
}
}
}, 1000);
}
// set the stage is ready to see the completed stage orders
ready_stage(e) {
var self = this;
self.state.stages = 'ready';
updateCountdownState(orderId, totalSeconds, isCompleted = false) {
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
this.state.countdowns = {
...this.state.countdowns,
[orderId]: {
minutes,
seconds,
isCompleted
}
};
}
//set the stage is waiting to see the ready stage orders
waiting_stage(e) {
var self = this;
self.state.stages = 'waiting';
onPosOrderCreation(message) {
if (!message || message.config_id !== this.currentShopId) {
return;
}
const relevantMessages = [
'pos_order_created',
'pos_order_updated',
'pos_order_paid',
'pos_order_accepted',
'pos_order_cancelled',
'pos_order_completed',
'pos_order_line_updated'
];
if ((message.res_model === "pos.order" || message.res_model === "pos.order.line") &&
relevantMessages.includes(message.message)) {
this.loadOrders();
}
}
//set the stage is draft to see the cooking stage orders
draft_stage(e) {
var self = this;
self.state.stages = 'draft';
async accept_order(e) {
const orderId = Number(e.target.value);
try {
await this.orm.call("pos.order", "order_progress_draft", [orderId]);
const order = this.state.order_details.find(o => o.id === orderId);
if (order) {
order.order_status = 'waiting';
if (order.avg_prepare_time) {
this.startCountdown(orderId, order.avg_prepare_time, order.config_id);
}
}
setTimeout(() => this.loadOrders(), 500);
} catch (error) {
console.error("Error accepting order:", error);
}
}
// change the status of the order from the kitchen
done_order(e) {
var self = this;
var input_id = $("#" + e.target.id).val();
this.orm.call("pos.order", "order_progress_change", [Number(input_id)])
var current_order = this.state.order_details.filter((order) => order.id==input_id)
if(current_order){
current_order[0].order_status = 'ready'
}
async done_order(e) {
const orderId = Number(e.target.value);
try {
await this.orm.call("pos.order", "order_progress_change", [orderId]);
const order = this.state.order_details.find(o => o.id === orderId);
if (order) {
order.order_status = 'ready';
this.updateCountdownState(orderId, 0, true);
if (this.countdownIntervals[orderId]) {
clearInterval(this.countdownIntervals[orderId]);
delete this.countdownIntervals[orderId];
}
}
setTimeout(() => this.loadOrders(), 500);
} catch (error) {
console.error("Error completing order:", error);
}
}
// change the status of the product from the kitchen
accept_order_line(e) {
var input_id = $("#" + e.target.id).val();
this.orm.call("pos.order.line", "order_progress_change", [Number(input_id)])
var current_order_line=this.state.lines.filter((order_line) => order_line.id==input_id)
if (current_order_line){
if (current_order_line[0].order_status == 'ready'){
current_order_line[0].order_status = 'waiting'
async cancel_order(e) {
const orderId = Number(e.target.value);
try {
await this.orm.call("pos.order", "order_progress_cancel", [orderId]);
const order = this.state.order_details.find(o => o.id === orderId);
if (order) {
order.order_status = 'cancel';
}
else{
current_order_line[0].order_status = 'ready'
setTimeout(() => this.loadOrders(), 500);
} catch (error) {
console.error("Error cancelling order:", error);
}
}
async accept_order_line(e) {
const lineId = Number(e.target.value);
try {
await this.orm.call("pos.order.line", "order_progress_change", [lineId]);
const line = this.state.lines.find(l => l.id === lineId);
if (line) {
line.order_status = line.order_status === 'ready' ? 'waiting' : 'ready';
}
setTimeout(() => this.loadOrders(), 500);
} catch (error) {
console.error("Error updating order line:", error);
}
}
get filteredOrders() {
return this.state.order_details.filter(order => {
const configMatch = Array.isArray(order.config_id) ?
order.config_id[0] === this.currentShopId :
order.config_id === this.currentShopId;
const stageMatch = order.order_status === this.state.stages;
return configMatch && stageMatch && order.order_status !== 'cancel';
});
}
forceRefresh() {
this.loadOrders();
}
}
kitchen_screen_dashboard.template = 'KitchenCustomDashBoard';
registry.category("actions").add("kitchen_custom_dashboard_tags", kitchen_screen_dashboard);
KitchenScreenDashboard.template = 'KitchenCustomDashBoard';
registry.category("actions").add("kitchen_custom_dashboard_tags", KitchenScreenDashboard);

41
pos_kitchen_screen_odoo/static/src/js/order_button.js

@ -38,25 +38,27 @@ setup() {
if (!this.clicked) {
this.clicked = true;
try {
var order_name=this.pos.selectedOrder.name
await self.orm.call("pos.order", "check_order_status", ["", this.pos.get_order().pos_reference]).then(function(result){
if (result==false){
self.kitchen_order_status=false
if (result == false){
self.kitchen_order_status = false
self.env.services.dialog.add(AlertDialog, {
title: _t("Order is Completed"),
body: _t("This Order is Completed. Please create a new Order"),
});
}
else{
self.kitchen_order_status=true
self.kitchen_order_status = true
}
});
if ( self.kitchen_order_status){
await this.pos.sendOrderInPreparationUpdateLastChange(this.currentOrder);
for (const orders of this.pos.get_order().lines) {
if (self.kitchen_order_status){
await this.pos.sendOrderInPreparationUpdateLastChange(this.currentOrder);
for (const orders of this.pos.get_order().lines) {
let actualQty = orders.qty || orders.quantity || orders.get_quantity() || 1;
line.push([0, 0, {
'qty': orders.quantity,
'qty': actualQty,
'price_unit': orders.price_unit,
'price_subtotal': orders.price_subtotal,
'price_subtotal_incl': orders.price_subtotal_incl,
@ -69,15 +71,16 @@ setup() {
'pack_lot_ids': [],
'full_product_name': orders.product_id.display_name,
'price_extra': orders.price_extra,
'name': 'newsx/0031',
'name': orders.product_id.display_name,
'is_cooking': true,
'note':orders.note
'note': orders.note
}])
}
const date = new Date(self.currentOrder.date_order.replace(' ', 'T'));
var orders = [{
'pos_reference': this.pos.get_order().pos_reference,
'session_id':this.pos.get_order().session_id.id,
'session_id': this.pos.get_order().session_id.id,
'amount_total': this.pos.get_order().amount_total,
'amount_paid': this.pos.get_order().amount_paid,
'amount_return': this.pos.get_order().amount_return,
@ -86,14 +89,16 @@ setup() {
'is_cooking': true,
'order_status': 'draft',
'company_id': this.pos.company.id,
'hour':date.getHours(),
'minutes':date.getMinutes(),
'table_id':this.pos.get_order().table_id.id,
'floor':this.pos.get_order().table_id.floor_id.name,
'config_id':this.pos.get_order().config_id.id
'hour': date.getHours(),
'minutes': date.getMinutes(),
'table_id': this.pos.get_order().table_id.id,
'floor': this.pos.get_order().table_id.floor_id.name,
'config_id': this.pos.get_order().config_id.id
}]
await self.orm.call("pos.order", "get_details", ["", self.pos.config.id, orders])
}
await self.orm.call("pos.order", "create_or_update_kitchen_order", [orders]);
this.env.bus.trigger('pos-kitchen-screen-update');
}
} finally {
this.clicked = false;
}

94
pos_kitchen_screen_odoo/static/src/xml/kitchen_screen_templates.xml

@ -263,7 +263,9 @@
role="img"
aria-label="Note"
title="Note"/>
<span style="font-size:14px"><t t-esc="line.note"/></span>
<span style="font-size:14px">
<t t-esc="line.note"/>
</span>
</t>
</button>
</ul>
@ -375,6 +377,30 @@
<t t-esc="order.floor"/>
</span>
</li>
<li>
<b>
<span style="font-size: 24px;">
⏰:
<span t-if="state.countdowns[order.id]">
<t t-if="state.countdowns[order.id].isCompleted">
<span style="color: green;">
00:00 (Ready)
</span>
</t>
<t t-else="">
<t t-esc="state.countdowns[order.id].minutes.toString().padStart(2, '0')"/>
:
<t t-esc="state.countdowns[order.id].seconds.toString().padStart(2, '0')"/>
Mins.
</t>
</span>
<span t-else="">
<t t-esc="order.avg_prepare_time.toFixed(2)"/>
Mins.
</span>
</span>
</b>
</li>
</ul>
</div>
</div>
@ -424,12 +450,25 @@
</span>
<t t-esc="line.full_product_name"/>
<br/>
<t t-foreach="state.prepare_times"
t-as="time"
t-key="time.id">
<t t-if="time.id == line.product_id[0]">
<span>⏰ :
<t t-esc="time.prepare_time"/>
Mins.
</span>
</t>
</t>
<br/>
<t t-if="line.note">
<i class="fa fa-tag fa-lg"
role="img"
aria-label="Note"
title="Note"/>
<span style="font-size:14px"><t t-esc="line.note"/></span>
<span style="font-size:14px">
<t t-esc="line.note"/>
</span>
</t>
</button>
</t>
@ -451,9 +490,11 @@
role="img"
aria-label="Note"
title="Note"/>
<span style="font-size:14px"><t t-esc="line.note"/></span>
<span style="font-size:14px">
<t t-esc="line.note"/>
</span>
</t>
<br/>
</button>
</t>
@ -540,6 +581,30 @@
<t t-esc="order.floor"/>
</span>
</li>
<li>
<b>
<span style="font-size: 24px;">
⏰:
<span t-if="state.countdowns[order.id]">
<t t-if="state.countdowns[order.id].isCompleted">
<span style="color: green;">
00:00 (Ready)
</span>
</t>
<t t-else="">
<t t-esc="state.countdowns[order.id].minutes.toString().padStart(2, '0')"/>
:
<t t-esc="state.countdowns[order.id].seconds.toString().padStart(2, '0')"/>
Mins.
</t>
</span>
<span t-else="">
<t t-esc="order.avg_prepare_time.toFixed(2)"/>
Mins.
</span>
</span>
</b>
</li>
</ul>
</div>
</div>
@ -568,14 +633,26 @@
</span>
<t t-esc="line.full_product_name"/>
<br/>
<t t-foreach="state.prepare_times"
t-as="time"
t-key="time.id">
<t t-if="time.id == line.product_id[0]">
<span>⏰ :
<t t-esc="time.prepare_time"/>
Mins.
</span>
</t>
</t>
<br/>
<t t-if="line.note">
<i class="fa fa-tag fa-lg"
role="img"
aria-label="Note"
title="Note"/>
<span style="font-size:14px"><t t-esc="line.note"/></span>
<span style="font-size:14px">
<t t-esc="line.note"/>
</span>
</t>
</button>
</t>
</t>
@ -685,12 +762,15 @@
</span>
<t t-esc="line.full_product_name"/>
<br/>
<br/>
<t t-if="line.note">
<i class="fa fa-tag fa-lg"
role="img"
aria-label="Note"
title="Note"/>
<span style="font-size:14px"><t t-esc="line.note"/></span>
<span style="font-size:14px">
<t t-esc="line.note"/>
</span>
</t>
</button>
</t>

15
pos_kitchen_screen_odoo/views/kitchen_screen_views.xml

@ -54,7 +54,20 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box"
id="is_preparation_complete">
<div class="o_setting_left_pane">
</div>
<div class="o_setting_right_pane">
<label for="is_preparation_complete"/>
<div class="text-muted">
<div>
<field name="is_preparation_complete"/>
</div>
Change the cooking stage when completing the preparation time
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box"
id="kitchen_screen">
<div class="o_setting_left_pane">

13
pos_kitchen_screen_odoo/views/product_product_views.xml

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="product_product_view_form_inherit" model="ir.ui.view">
<field name="name">product.product.form.inherit</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="product.product_normal_form_view"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='to_weight']" position="after">
<field name="prepair_time_minutes" widget="float_time"/>
</xpath>
</field>
</record>
</odoo>
Loading…
Cancel
Save