# -*- coding: utf-8 -*- """ Fix for KDS (Kitchen Display System) / Preparation Display bug: Problem: When Table X has N items that are already in the "Completed" stage on the KDS, and the customer adds a new item, Odoo creates a new pos.prep.order for the same pos.order. The KDS frontend receives a LOAD_ORDERS notification and reloads all open prep states for that pos.order via get_preparation_display_order(). The _get_open_orderlines_in_display() filter excludes ONLY states where: todo=False AND stage=last_stage Items that were advanced to the last stage via the stage-advance button have: todo=True, stage=last_stage <-- these ARE returned and shown as "To Prepare" 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: 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' def _process_preparation_changes(self, options): """ Override to fix KDS stage reset bug. 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() # 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 # 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) # 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. ]) # 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, ) # 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