From c6aa68fe39e2d86fc7ac9339247e097bd655d2b0 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Thu, 7 May 2026 12:06:03 +0700 Subject: [PATCH] refactor: offload KDS report updates to background thread and simplify pivot view fields --- models/pos_kds_report.py | 26 ++--- models/pos_prep_state.py | 181 ++++++++++++++++++++++----------- views/pos_kds_report_views.xml | 8 +- 3 files changed, 136 insertions(+), 79 deletions(-) diff --git a/models/pos_kds_report.py b/models/pos_kds_report.py index 82c8060..0181a2d 100644 --- a/models/pos_kds_report.py +++ b/models/pos_kds_report.py @@ -6,24 +6,24 @@ class PosKdsReportLine(models.Model): _description = 'KDS Line Completion Report' _order = 'completion_datetime desc' - pos_order_id = fields.Many2one('pos.order', string='Order', required=True, ondelete='cascade') - pos_order_line_id = fields.Many2one('pos.order.line', string='Order Line', required=True, ondelete='cascade') - product_id = fields.Many2one('product.product', string='Product', required=True) - pos_category_id = fields.Many2one('pos.category', string='POS Category', compute='_compute_pos_category', store=True) - pos_config_id = fields.Many2one('pos.config', string='POS Shop', compute='_compute_pos_config', store=True) - prep_display_id = fields.Many2one('pos.prep.display', string='Preparation Display', required=True) + pos_order_id = fields.Many2one('pos.order', string='Order', required=True, ondelete='cascade', index=True) + pos_order_line_id = fields.Many2one('pos.order.line', string='Order Line', required=True, ondelete='cascade', index=True) + product_id = fields.Many2one('product.product', string='Product', required=True, index=True) + pos_category_id = fields.Many2one('pos.category', string='POS Category', compute='_compute_pos_category', store=True, index=True) + pos_config_id = fields.Many2one('pos.config', string='POS Shop', compute='_compute_pos_config', store=True, index=True) + prep_display_id = fields.Many2one('pos.prep.display', string='Preparation Display', required=True, index=True) state = fields.Selection([ ('in_prep', 'In Preparation'), ('ready', 'Ready'), ('done', 'Completed'), ('cancelled', 'Cancelled') - ], string='Status', default='done') + ], string='Status', default='done', index=True) preparation_time = fields.Integer('Preparation Time (s)', help="Seconds taken to prepare") service_time = fields.Integer('Service Time (s)', help="Seconds taken to serve") completion_time = fields.Integer('Completion Time (s)', help="Total seconds taken to complete (prep + service)") - completion_datetime = fields.Datetime('Completion Date', default=fields.Datetime.now) + completion_datetime = fields.Datetime('Completion Date', default=fields.Datetime.now, index=True) @api.depends('product_id') def _compute_pos_category(self): @@ -46,21 +46,21 @@ class PosKdsReportOrder(models.Model): _description = 'KDS Order Completion Report' _order = 'completion_datetime desc' - pos_order_id = fields.Many2one('pos.order', string='Order', required=True, ondelete='cascade') - pos_config_id = fields.Many2one('pos.config', string='POS Shop', compute='_compute_pos_config', store=True) - prep_display_id = fields.Many2one('pos.prep.display', string='Preparation Display', required=True) + pos_order_id = fields.Many2one('pos.order', string='Order', required=True, ondelete='cascade', index=True) + pos_config_id = fields.Many2one('pos.config', string='POS Shop', compute='_compute_pos_config', store=True, index=True) + prep_display_id = fields.Many2one('pos.prep.display', string='Preparation Display', required=True, index=True) state = fields.Selection([ ('in_prep', 'In Preparation'), ('ready', 'Ready'), ('done', 'Completed'), ('cancelled', 'Cancelled') - ], string='Status', default='done') + ], string='Status', default='done', index=True) preparation_time = fields.Integer('Preparation Time (s)', help="Max preparation time across lines") service_time = fields.Integer('Service Time (s)', help="Max service time across lines") completion_time = fields.Integer('Completion Time (s)', help="Max completion time across all lines of the order on this display") - completion_datetime = fields.Datetime('Completion Date', default=fields.Datetime.now) + completion_datetime = fields.Datetime('Completion Date', default=fields.Datetime.now, index=True) @api.depends('pos_order_id') def _compute_pos_config(self): diff --git a/models/pos_prep_state.py b/models/pos_prep_state.py index 9e2e5b3..916dfd8 100644 --- a/models/pos_prep_state.py +++ b/models/pos_prep_state.py @@ -1,36 +1,53 @@ -from odoo import models +import threading +import logging +from odoo import models, fields, api +_logger = logging.getLogger(__name__) -class PosPreparationState(models.Model): - _inherit = 'pos.prep.state' - - def _update_kds_report(self, pdis_state): - if not pdis_state.prep_line_id.pos_order_line_id: - return - - order_line = pdis_state.prep_line_id.pos_order_line_id - prep_display = pdis_state.stage_id.prep_display_id - - is_completed = False - if len(pdis_state.stage_id.prep_display_id.stage_ids) > 1: - if pdis_state.stage_id.is_stage_position(-1): - is_completed = True - elif pdis_state.stage_id.is_stage_position(-2) and not pdis_state.todo: - is_completed = True - else: - if pdis_state.stage_id.is_stage_position(0) and not pdis_state.todo: - is_completed = True +def _threaded_report_update(registry, uid, context, pdis_state_id, is_reset, is_completed): + """Background worker to update KDS reports without blocking the POS transaction.""" + try: + with registry.cursor() as new_cr: + new_env = api.Environment(new_cr, uid, context) + pdis_state = new_env['pos.prep.state'].browse(pdis_state_id) + if not pdis_state.exists(): + return - is_reset = False - if pdis_state.stage_id.is_stage_position(0) and pdis_state.todo: - is_reset = True + order_line = pdis_state.prep_line_id.pos_order_line_id + stage = pdis_state.stage_id + display_id = stage.prep_display_id.id - line_report = self.env['pos.kds.report.line'].search([ - ('pos_order_line_id', '=', order_line.id), - ('prep_display_id', '=', prep_display.id) - ], limit=1) - - if is_completed: + KdsLineReport = new_env['pos.kds.report.line'].sudo() + KdsOrderReport = new_env['pos.kds.report.order'].sudo() + + if is_reset: + line_report = KdsLineReport.search([ + ('pos_order_line_id', '=', order_line.id), + ('prep_display_id', '=', display_id) + ], limit=1) + + if line_report: + line_report.unlink() + + order_report = KdsOrderReport.search([ + ('pos_order_id', '=', order_line.order_id.id), + ('prep_display_id', '=', display_id) + ], limit=1) + + if order_report: + res = KdsLineReport._read_group( + [('pos_order_id', '=', order_line.order_id.id), ('prep_display_id', '=', display_id)], + aggregates=['completion_time:max'] + ) + max_comp_time = res[0][0] if res else 0 + if max_comp_time == 0: + order_report.unlink() + else: + order_report.write({'completion_time': max_comp_time}) + new_cr.commit() + return + + # If completed prep_time = max(0, order_line.preparation_time) svc_time = max(0, order_line.service_time) comp_time = prep_time + svc_time @@ -39,60 +56,104 @@ class PosPreparationState(models.Model): 'pos_order_id': order_line.order_id.id, 'pos_order_line_id': order_line.id, 'product_id': order_line.product_id.id, - 'prep_display_id': prep_display.id, + 'prep_display_id': display_id, 'preparation_time': prep_time, 'service_time': svc_time, 'completion_time': comp_time, + 'completion_datetime': fields.Datetime.now(), } + + line_report = KdsLineReport.search([ + ('pos_order_line_id', '=', order_line.id), + ('prep_display_id', '=', display_id) + ], limit=1) + if line_report: line_report.write(vals) else: - self.env['pos.kds.report.line'].create(vals) + KdsLineReport.create(vals) # Update order-level report - order_report = self.env['pos.kds.report.order'].search([ + order_report = KdsOrderReport.search([ ('pos_order_id', '=', order_line.order_id.id), - ('prep_display_id', '=', prep_display.id) + ('prep_display_id', '=', display_id) ], limit=1) - # Re-calculate max completion time for the order on this KDS - all_line_reports = self.env['pos.kds.report.line'].search([ - ('pos_order_id', '=', order_line.order_id.id), - ('prep_display_id', '=', prep_display.id) - ]) - max_comp_time = max(all_line_reports.mapped('completion_time')) if all_line_reports else comp_time + res = KdsLineReport._read_group( + [('pos_order_id', '=', order_line.order_id.id), ('prep_display_id', '=', display_id)], + aggregates=['completion_time:max'] + ) + max_comp_time = res[0][0] if res else comp_time order_vals = { 'pos_order_id': order_line.order_id.id, - 'prep_display_id': prep_display.id, + 'prep_display_id': display_id, 'completion_time': max_comp_time, + 'completion_datetime': fields.Datetime.now(), } if order_report: order_report.write(order_vals) else: - self.env['pos.kds.report.order'].create(order_vals) + KdsOrderReport.create(order_vals) - elif is_reset: - if line_report: - line_report.unlink() + new_cr.commit() + except Exception as e: + _logger.error("Background KDS reporting failed: %s", e) + +class PosPreparationState(models.Model): + _inherit = 'pos.prep.state' + + def _update_kds_report(self, pdis_state): + if not pdis_state.prep_line_id or not pdis_state.prep_line_id.pos_order_line_id: + return - # We don't necessarily delete the order record since other lines might still be complete. - # But we could re-calculate the max if needed. Let's re-eval. - all_line_reports = self.env['pos.kds.report.line'].search([ - ('pos_order_id', '=', order_line.order_id.id), - ('prep_display_id', '=', prep_display.id) - ]) - order_report = self.env['pos.kds.report.order'].search([ - ('pos_order_id', '=', order_line.order_id.id), - ('prep_display_id', '=', prep_display.id) - ], limit=1) + stage = pdis_state.stage_id + if not stage: + return + + # Determine status synchronously to decide if we need a thread + if not hasattr(self.env, '_kds_display_cache'): + self.env._kds_display_cache = {} + + display_id = stage.prep_display_id.id + if display_id not in self.env._kds_display_cache: + prep_display = stage.prep_display_id + stage_ids = prep_display.stage_ids.ids + self.env._kds_display_cache[display_id] = { + 'num_stages': len(stage_ids), + 'first_stage_id': stage_ids[0] if stage_ids else False, + 'last_stage_id': stage_ids[-1] if stage_ids else False, + 'second_last_stage_id': stage_ids[-2] if len(stage_ids) > 1 else False, + } + + display_info = self.env._kds_display_cache[display_id] + num_stages = display_info['num_stages'] + + is_completed = False + if num_stages > 1: + if stage.id == display_info['last_stage_id']: + is_completed = True + elif display_info['second_last_stage_id'] and stage.id == display_info['second_last_stage_id'] and not pdis_state.todo: + is_completed = True + elif num_stages == 1: + if not pdis_state.todo: + is_completed = True + + is_reset = False + if stage.id == display_info['first_stage_id'] and pdis_state.todo: + is_reset = True - if order_report: - if not all_line_reports: - order_report.unlink() - else: - max_comp_time = max(all_line_reports.mapped('completion_time')) - order_report.write({'completion_time': max_comp_time}) + if not (is_completed or is_reset): + return + + # Start background thread to handle the reporting DB operations + # This makes the main POS sync call nearly instant + thread = threading.Thread( + target=_threaded_report_update, + args=(self.env.registry, self.env.uid, self.env.context, pdis_state.id, is_reset, is_completed) + ) + thread.daemon = True + thread.start() def _record_status_change_prep_time(self, pdis_state): super()._record_status_change_prep_time(pdis_state) diff --git a/views/pos_kds_report_views.xml b/views/pos_kds_report_views.xml index ca1f53e..e442b52 100644 --- a/views/pos_kds_report_views.xml +++ b/views/pos_kds_report_views.xml @@ -11,9 +11,7 @@ - - @@ -25,7 +23,7 @@ - + @@ -78,9 +76,7 @@ - - @@ -91,7 +87,7 @@ - +