From 4df87137874c14c4cb26dbfd650a382e5f5f08e0 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Mon, 1 Jun 2026 22:00:18 +0700 Subject: [PATCH] fix: resolve KDS stage reset bug by adding dependency and enforcing state closure after downstream overrides. --- __manifest__.py | 2 +- models/pos_order.py | 121 +++++++++++++++++++++++++++++--------------- 2 files changed, 80 insertions(+), 43 deletions(-) diff --git a/__manifest__.py b/__manifest__.py index 116e2c5..b7ae00a 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -15,7 +15,7 @@ and todo status of previously processed prep order lines. 'website': '', 'category': 'Sales/Point of Sale', 'version': '19.0.1.0.0', - 'depends': ['pos_enterprise'], + 'depends': ['pos_enterprise', 'custom_preparation_display'], 'data': [], 'assets': {}, 'installable': True, diff --git a/models/pos_order.py b/models/pos_order.py index 568593b..1506066 100644 --- a/models/pos_order.py +++ b/models/pos_order.py @@ -17,16 +17,26 @@ Problem: So the previously completed items (stage-advanced to last stage, todo=True) reappear on the KDS card alongside the new item, effectively resetting their visual status. +Additional complication: + The custom_preparation_display module ALSO overrides _process_preparation_changes + and runs AFTER pos_kds_fix in the MRO (since it calls super() which reaches here). + After super() returns, custom_preparation_display re-searches ALL prep orders + (including old ones) and syncs parent combo states. This can revert the todo=False + we set on old last-stage states back to todo=True, defeating the fix. + Fix: - When a new pos.prep.order is created for an existing pos.order (meaning new items - are being added to a table that already sent items to the kitchen), we find all - existing prep states for all PREVIOUS prep orders of the same pos.order that are - currently at the LAST stage (regardless of todo status). These represent items - the kitchen staff already processed. We set their todo=False to ensure the filter - in _get_open_orderlines_in_display() correctly excludes them from future reloads. + 1. Capture existing prep orders BEFORE super() creates a new one. + 2. After all supers() have run (including custom_preparation_display's combo sync), + do a FINAL enforcement pass that forces all old last-stage states to todo=False. + This runs last in the call chain so no subsequent override can undo it. + 3. Only target states from genuinely OLD prep orders (pre-existing before this call). """ +import logging + from odoo import models +_logger = logging.getLogger(__name__) + class PosOrder(models.Model): _inherit = 'pos.order' @@ -35,51 +45,78 @@ class PosOrder(models.Model): """ Override to fix KDS stage reset bug. - Before calling super(), we record existing prep orders for this pos.order. - After super() runs (which may create a new prep order for new items), - we mark all states in the OLD prep orders that are at the last stage - as todo=False so they won't reappear on the KDS display. + We capture existing prep orders BEFORE super() (which may include + custom_preparation_display's combo sync and pos_enterprise's core logic). + After the entire super() chain returns, we do a final enforcement pass + to ensure all old last-stage states remain todo=False regardless of + what any intermediate override may have set them to. """ self.ensure_one() - # Capture existing prep orders BEFORE super() creates a new one - existing_prep_orders = self.env['pos.prep.order'].search([ + # Step 1: Capture existing prep orders BEFORE super() creates a new one. + # These are the "old" prep orders whose states should not be reset. + existing_prep_order_ids = self.env['pos.prep.order'].search([ ('pos_order_id', '=', self.id) - ]) + ]).ids - # Run the original logic (may create new pos.prep.order + lines + states) + # Step 2: Run the full super() chain. + # This includes: + # - custom_preparation_display._process_preparation_changes (combo sync) + # - pos_enterprise._process_preparation_changes (new prep order creation) result = super()._process_preparation_changes(options) - # If new items were added (flag_order_added), super() created a new prep order - if result.get('order_added') and existing_prep_orders: - # Fetch all prep displays relevant to this order - prep_displays = self.env['pos.prep.display'].search([ - '|', - ('pos_config_ids', '=', False), - ('pos_config_ids', 'in', self.config_id.id), + # Step 3: Only act if new items were added (a new prep order was created). + if not result.get('order_added') or not existing_prep_order_ids: + return result + + # Step 4: Fetch all prep displays relevant to this order's POS config. + prep_displays = self.env['pos.prep.display'].search([ + '|', + ('pos_config_ids', '=', False), + ('pos_config_ids', 'in', self.config_id.id), + ]) + + for prep_display in prep_displays: + stage_ids = prep_display.stage_ids.ids + if not stage_ids: + continue + last_stage_id = stage_ids[-1] + + # Step 5: Find ALL states from OLD prep orders that are at the last stage. + # This covers both: + # (a) States with todo=True — kitchen staff used stage-advance button + # (b) States with todo=False — already correctly excluded by the filter + # but may have been re-set to True by custom_preparation_display's + # combo-parent sync logic. + # We unconditionally force them all to todo=False so the filter + # in _get_open_orderlines_in_display() correctly excludes them. + old_prep_lines = self.env['pos.prep.line'].search([ + ('prep_order_id', 'in', existing_prep_order_ids), + ]) + if not old_prep_lines: + continue + + states_to_seal = self.env['pos.prep.state'].search([ + ('prep_line_id', 'in', old_prep_lines.ids), + ('stage_id', '=', last_stage_id), + # Include both todo=True AND todo=False — if custom_preparation_display + # reverted a False back to True, we need to catch it. + # Using a simple search with no todo filter is intentional. ]) - for prep_display in prep_displays: - last_stage_id = ( - prep_display.stage_ids.ids[-1] - if prep_display.stage_ids.ids else False + # Separate the ones that still need updating to avoid unnecessary writes. + states_needing_fix = states_to_seal.filtered(lambda s: s.todo) + if states_needing_fix: + _logger.info( + "pos_kds_fix: Sealing %d old last-stage states as todo=False " + "for pos.order %s (prep orders: %s)", + len(states_needing_fix), + self.name or self.id, + existing_prep_order_ids, ) - if not last_stage_id: - continue - - # Find all states from the OLD prep orders that are at the last stage - # with todo=True — these are items the kitchen advanced to "Completed" - # stage but which would still be returned by _get_open_orderlines_in_display - # because the filter only excludes (todo=False AND stage=last). - old_prep_lines = existing_prep_orders.mapped('prep_line_ids') - states_at_last_stage = self.env['pos.prep.state'].search([ - ('prep_line_id', 'in', old_prep_lines.ids), - ('stage_id', '=', last_stage_id), - ('todo', '=', True), - ]) - - if states_at_last_stage: - # Mark them as done so the KDS filter hides them - states_at_last_stage.write({'todo': False}) + # Use direct ORM write — no websocket notification is needed here. + # The frontend will get a LOAD_ORDERS notification from process_order() + # anyway, and get_preparation_display_order() will re-read from DB. + states_needing_fix.write({'todo': False}) return result