diff --git a/pos_kitchen_screen_odoo/__manifest__.py b/pos_kitchen_screen_odoo/__manifest__.py index ecf006857..8e9d10af2 100644 --- a/pos_kitchen_screen_odoo/__manifest__.py +++ b/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': [ diff --git a/pos_kitchen_screen_odoo/doc/RELEASE_NOTES.md b/pos_kitchen_screen_odoo/doc/RELEASE_NOTES.md index 750572e1b..14745ca1b 100644 --- a/pos_kitchen_screen_odoo/doc/RELEASE_NOTES.md +++ b/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. diff --git a/pos_kitchen_screen_odoo/models/__init__.py b/pos_kitchen_screen_odoo/models/__init__.py index a64447604..c31135d74 100644 --- a/pos_kitchen_screen_odoo/models/__init__.py +++ b/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 diff --git a/pos_kitchen_screen_odoo/models/kitchen_screen.py b/pos_kitchen_screen_odoo/models/kitchen_screen.py index 7ecdd48f8..3d5810269 100644 --- a/pos_kitchen_screen_odoo/models/kitchen_screen.py +++ b/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 { diff --git a/pos_kitchen_screen_odoo/models/pos_orders.py b/pos_kitchen_screen_odoo/models/pos_orders.py index 45f46d62f..9f433c2bf 100644 --- a/pos_kitchen_screen_odoo/models/pos_orders.py +++ b/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) \ No newline at end of file diff --git a/pos_kitchen_screen_odoo/models/pos_session.py b/pos_kitchen_screen_odoo/models/pos_session.py index 67ac2d5a0..230a59570 100644 --- a/pos_kitchen_screen_odoo/models/pos_session.py +++ b/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): diff --git a/pos_kitchen_screen_odoo/models/product_product.py b/pos_kitchen_screen_odoo/models/product_product.py new file mode 100644 index 000000000..32bf7bb63 --- /dev/null +++ b/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)") \ No newline at end of file diff --git a/pos_kitchen_screen_odoo/static/description/index.html b/pos_kitchen_screen_odoo/static/description/index.html index 3455f1689..bc51f0e81 100644 --- a/pos_kitchen_screen_odoo/static/description/index.html +++ b/pos_kitchen_screen_odoo/static/description/index.html @@ -960,6 +960,41 @@ +
+
+

+ Latest Release 18.0.1.1.3 +

+ + 25th July, 2025 + +
+
+
+
+
+ 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. +
  • +
+
+
+
+
+
+
diff --git a/pos_kitchen_screen_odoo/static/src/js/kitchen_screen.js b/pos_kitchen_screen_odoo/static/src/js/kitchen_screen.js index 1d9615d38..21b5fc853 100644 --- a/pos_kitchen_screen_odoo/static/src/js/kitchen_screen.js +++ b/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); \ No newline at end of file + +KitchenScreenDashboard.template = 'KitchenCustomDashBoard'; +registry.category("actions").add("kitchen_custom_dashboard_tags", KitchenScreenDashboard); \ No newline at end of file diff --git a/pos_kitchen_screen_odoo/static/src/js/order_button.js b/pos_kitchen_screen_odoo/static/src/js/order_button.js index a9b1cbb70..2b53c7ac5 100644 --- a/pos_kitchen_screen_odoo/static/src/js/order_button.js +++ b/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; } diff --git a/pos_kitchen_screen_odoo/static/src/xml/kitchen_screen_templates.xml b/pos_kitchen_screen_odoo/static/src/xml/kitchen_screen_templates.xml index c83e6cca0..c1f4efe56 100644 --- a/pos_kitchen_screen_odoo/static/src/xml/kitchen_screen_templates.xml +++ b/pos_kitchen_screen_odoo/static/src/xml/kitchen_screen_templates.xml @@ -263,7 +263,9 @@ role="img" aria-label="Note" title="Note"/> - + + + @@ -375,6 +377,30 @@ +
  • + + + ⏰: + + + + 00:00 (Ready) + + + + + : + + Mins. + + + + + Mins. + + + +
  • @@ -424,12 +450,25 @@
    + + + ⏰ : + + Mins. + + + +
    - + + +
    @@ -451,9 +490,11 @@ role="img" aria-label="Note" title="Note"/> - + + +
    - +
    @@ -540,6 +581,30 @@ +
  • + + + ⏰: + + + + 00:00 (Ready) + + + + + : + + Mins. + + + + + Mins. + + + +
  • @@ -568,14 +633,26 @@
    + + + ⏰ : + + Mins. + + + +
    - + + + -
    @@ -685,12 +762,15 @@
    +
    - + + +
    diff --git a/pos_kitchen_screen_odoo/views/kitchen_screen_views.xml b/pos_kitchen_screen_odoo/views/kitchen_screen_views.xml index 2e2ebddab..941dc9543 100644 --- a/pos_kitchen_screen_odoo/views/kitchen_screen_views.xml +++ b/pos_kitchen_screen_odoo/views/kitchen_screen_views.xml @@ -54,7 +54,20 @@ - +
    +
    +
    +
    +
    +
    diff --git a/pos_kitchen_screen_odoo/views/product_product_views.xml b/pos_kitchen_screen_odoo/views/product_product_views.xml new file mode 100644 index 000000000..700d56f1c --- /dev/null +++ b/pos_kitchen_screen_odoo/views/product_product_views.xml @@ -0,0 +1,13 @@ + + + + product.product.form.inherit + product.product + + + + + + + + \ No newline at end of file