From 613ffa197fcdb0c174e8b32a3c6d1bddada01355 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Mon, 25 May 2026 20:50:40 +0700 Subject: [PATCH] feat: optimize 2-stage KDS reporting and implement average-based duration measures --- README.md | 35 ++++++++++++++++++++---- models/pos_kds_report.py | 50 ++++++++++++++++++++++++++++++---- models/pos_prep_state.py | 26 ++++++++++++++++-- views/pos_kds_report_views.xml | 8 +++--- 4 files changed, 100 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 2999a32..705a956 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,44 @@ # POS KDS Tracker and Report ## Overview -Standard Odoo 19 deletes POS Preparation Display (KDS) records a day after completion, preventing historical analysis of preparation times. This module creates a persistent log of completed orders and order lines from the Kitchen Display System (KDS) to enable detailed historical reporting. +Standard Odoo 19 deletes POS Preparation Display (KDS) records a day after completion, preventing historical analysis of preparation times. This module creates a persistent log of completed orders and order lines from the Kitchen Display System (KDS) to enable detailed, multi-dimensional historical reporting. -Additionally, this module preserves timer continuity for orders and lines on the Kitchen Preparation Display UI when they are recalled or resetted back to preparation stages. +Additionally, this module preserves timer continuity for orders and lines on the Kitchen Preparation Display UI when they are recalled or resetted back to preparation stages, and fully optimizes reporting metrics for 2-stage KDS configurations. + +--- ## Features -- **Historical Persistence**: Permanently records Kitchen Display System (KDS) states and times, overcoming Odoo's default 1-day deletion behavior. -- **Average Preparation Time Reports**: Generates pivot and graph reports showing the average preparation time globally across all POS Shops, categories, products, or individual Kitchen Displays. -- **Timer Continuity on Reset/Recall**: When an order or order line is resetted or recalled back from Completed to a preparation stage, the UI timer automatically continues counting upwards from its last elapsed time instead of resetting back to `0`. -- **Advanced Filtering and Grouping**: Multi-dimension analysis by POS Shop, individual KDS Display, Product Category, Product, or Status. + +### 1. Timer Continuity on Reset/Recall (UI & Backend) +- When an order or order line is recalled or resetted back from **Completed** to a preparation stage, the KDS UI timer **continues counting upwards** from its last accumulated elapsed time instead of resetting back to `0`. +- The backend automatically stores, updates, and broadcasts the accumulated KDS elapsed time (`kds_accumulated_time`) via the Odoo Bus to keep the display perfectly in sync. + +### 2. 2-Stage KDS Optimization +- For simplified 2-stage KDS setups (**To Prepare** $\rightarrow$ **Completed**), the module automatically ensures: + $$\text{Preparation Time} = \text{Service Time} = \text{Completion Time}$$ +- This prevents the total elapsed time from being doubled in reports when no intermediate serving/ready stage exists. + +### 3. Comprehensive Dual Measures (Sum & Avg) +- Generates pivot and graph reports showing both **Sum** and **Average** metrics natively for every duration: + - **Completion Time (s)** (Sum) & **Average Completion Time (s)** (Avg) + - **Preparation Time (s)** (Sum) & **Average Preparation Time (s)** (Avg) + - **Service Time (s)** (Sum) & **Average Service Time (s)** (Avg) +- **Average Completion Time** is loaded by default as the main measure in pivot and graph views, but all other options can be fully toggled or combined under the *Measures* dropdown list. + +### 4. Automatic Historical Backfill Migration +- Upgrading the module automatically executes a post-install data migration. +- It copies historical sums into the new average-specific database columns, preventing the average columns from appearing blank for pre-existing records. + +--- ## Installation Install out-of-the-box like a standard Odoo module on an Odoo 19 environment possessing POS Enterprise (`pos_enterprise`). +--- + ## Usage 1. Open the Point of Sale and place orders to the KDS. 2. Complete stages in the Preparation Display. 3. If an order or line needs to be recalled or resetted back to preparation, the display UI timer will resume incrementing starting from the previous session's elapsed time. 4. Go to **Point of Sale > Reporting > KDS Product Analysis** or **KDS Order Analysis** to view Pivot and Graph summaries. +5. In the pivot table view, toggle **Measures** to add, remove, or compare **Sum** and **Average** durations dynamically. diff --git a/models/pos_kds_report.py b/models/pos_kds_report.py index 0181a2d..07a7f21 100644 --- a/models/pos_kds_report.py +++ b/models/pos_kds_report.py @@ -20,9 +20,12 @@ class PosKdsReportLine(models.Model): ('cancelled', 'Cancelled') ], 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)") + preparation_time = fields.Integer('Preparation Time (s)', group_operator='sum', help="Seconds taken to prepare") + preparation_time_avg = fields.Integer('Average Preparation Time (s)', group_operator='avg', help="Average seconds taken to prepare") + service_time = fields.Integer('Service Time (s)', group_operator='sum', help="Seconds taken to serve") + service_time_avg = fields.Integer('Average Service Time (s)', group_operator='avg', help="Average seconds taken to serve") + completion_time = fields.Integer('Completion Time (s)', group_operator='sum', help="Total seconds taken to complete (prep + service)") + completion_time_avg = fields.Integer('Average Completion Time (s)', group_operator='avg', help="Average total seconds taken to complete") completion_datetime = fields.Datetime('Completion Date', default=fields.Datetime.now, index=True) @api.depends('product_id') @@ -40,6 +43,22 @@ class PosKdsReportLine(models.Model): for record in self: record.display_name = f"{record.pos_order_id.name} - {record.product_id.name}" + def init(self): + super().init() + self.env.cr.execute(""" + UPDATE pos_kds_report_line + SET preparation_time_avg = COALESCE(preparation_time, 0) + WHERE preparation_time_avg IS NULL OR preparation_time_avg = 0; + + UPDATE pos_kds_report_line + SET service_time_avg = COALESCE(service_time, 0) + WHERE service_time_avg IS NULL OR service_time_avg = 0; + + UPDATE pos_kds_report_line + SET completion_time_avg = COALESCE(completion_time, 0) + WHERE completion_time_avg IS NULL OR completion_time_avg = 0; + """) + class PosKdsReportOrder(models.Model): _name = 'pos.kds.report.order' @@ -57,9 +76,12 @@ class PosKdsReportOrder(models.Model): ('cancelled', 'Cancelled') ], 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") + preparation_time = fields.Integer('Preparation Time (s)', group_operator='sum', help="Max preparation time across lines") + preparation_time_avg = fields.Integer('Average Preparation Time (s)', group_operator='avg', help="Average preparation time across lines") + service_time = fields.Integer('Service Time (s)', group_operator='sum', help="Max service time across lines") + service_time_avg = fields.Integer('Average Service Time (s)', group_operator='avg', help="Average service time across lines") + completion_time = fields.Integer('Completion Time (s)', group_operator='sum', help="Max completion time across all lines of the order on this display") + completion_time_avg = fields.Integer('Average Completion Time (s)', group_operator='avg', help="Average completion time across all lines of the order on this display") completion_datetime = fields.Datetime('Completion Date', default=fields.Datetime.now, index=True) @api.depends('pos_order_id') @@ -71,3 +93,19 @@ class PosKdsReportOrder(models.Model): def _compute_display_name(self): for record in self: record.display_name = f"{record.pos_order_id.name} ({record.prep_display_id.name})" + + def init(self): + super().init() + self.env.cr.execute(""" + UPDATE pos_kds_report_order + SET preparation_time_avg = COALESCE(preparation_time, 0) + WHERE preparation_time_avg IS NULL OR preparation_time_avg = 0; + + UPDATE pos_kds_report_order + SET service_time_avg = COALESCE(service_time, 0) + WHERE service_time_avg IS NULL OR service_time_avg = 0; + + UPDATE pos_kds_report_order + SET completion_time_avg = COALESCE(completion_time, 0) + WHERE completion_time_avg IS NULL OR completion_time_avg = 0; + """) diff --git a/models/pos_prep_state.py b/models/pos_prep_state.py index 9158504..3be9b78 100644 --- a/models/pos_prep_state.py +++ b/models/pos_prep_state.py @@ -108,7 +108,13 @@ class PosPreparationState(models.Model): prep_time = max(0, prep_time) svc_time = max(0, svc_time) - comp_time = prep_time + svc_time + + stage_count = self.env['pos.prep.stage'].search_count([('prep_display_id', '=', display_id)]) + if stage_count <= 2: + comp_time = prep_time + svc_time = prep_time + else: + comp_time = prep_time + svc_time line_report = KdsLineReport.search([ ('pos_order_line_id', '=', order_line.id), @@ -121,8 +127,11 @@ class PosPreparationState(models.Model): 'product_id': order_line.product_id.id, 'prep_display_id': display_id, 'preparation_time': prep_time, + 'preparation_time_avg': prep_time, 'service_time': svc_time, + 'service_time_avg': svc_time, 'completion_time': comp_time, + 'completion_time_avg': comp_time, 'completion_datetime': fields.Datetime.now(), 'state': 'done', } @@ -132,15 +141,21 @@ class PosPreparationState(models.Model): # Note: We only accumulate if the current session's time is valid. new_prep = line_report.preparation_time + prep_time new_svc = line_report.service_time + svc_time - new_comp = new_prep + new_svc + if stage_count <= 2: + new_comp = new_prep + else: + new_comp = new_prep + new_svc _logger.info("KDS Re-Completed (Accumulating): Order %s, Line %s: %ss + %ss = %ss", order_name, order_line.product_id.name, line_report.completion_time, comp_time, new_comp) line_report.write({ 'preparation_time': new_prep, + 'preparation_time_avg': new_prep, 'service_time': new_svc, + 'service_time_avg': new_svc, 'completion_time': new_comp, + 'completion_time_avg': new_comp, 'completion_datetime': vals['completion_datetime'], 'state': 'done', }) @@ -162,8 +177,10 @@ class PosPreparationState(models.Model): # We use the max completion time across all lines of this order for this display. # If we just updated a line to 219s, and order report was 204s, it becomes 219s. if comp_time > order_report.completion_time or order_report.state != 'done': + new_comp_time = max(comp_time, order_report.completion_time) order_report.write({ - 'completion_time': max(comp_time, order_report.completion_time), + 'completion_time': new_comp_time, + 'completion_time_avg': new_comp_time, 'state': 'done', 'completion_datetime': vals['completion_datetime'], }) @@ -172,8 +189,11 @@ class PosPreparationState(models.Model): 'pos_order_id': order_id, 'prep_display_id': display_id, 'preparation_time': prep_time, # Simplified: will be updated by other lines if needed + 'preparation_time_avg': prep_time, 'service_time': svc_time, + 'service_time_avg': svc_time, 'completion_time': comp_time, + 'completion_time_avg': comp_time, 'state': 'done', 'completion_datetime': vals['completion_datetime'], }) diff --git a/views/pos_kds_report_views.xml b/views/pos_kds_report_views.xml index e442b52..34e40b0 100644 --- a/views/pos_kds_report_views.xml +++ b/views/pos_kds_report_views.xml @@ -11,7 +11,7 @@ - + @@ -23,7 +23,7 @@ - + @@ -76,7 +76,7 @@ - + @@ -87,7 +87,7 @@ - +