feat: add KDS accumulated time tracking to preserve timer continuity during state transitions

This commit is contained in:
Suherdy Yacob 2026-05-25 19:29:03 +07:00
parent 32352f0449
commit c4201055fd
7 changed files with 86 additions and 7 deletions

View File

@ -3,16 +3,19 @@
## Overview ## 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 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.
## Features ## Features
- Records the state of the Kitchen Display System or Preparation Display permanently. - **Historical Persistence**: Permanently records Kitchen Display System (KDS) states and times, overcoming Odoo's default 1-day deletion behavior.
- Generates reports showing the average time to complete an order. - **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.
- Generates reports showing the average time to complete certain products or product categories. - **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`.
- Allows filtering and grouping globally for a POS Shop or individually per KDS. - **Advanced Filtering and Grouping**: Multi-dimension analysis by POS Shop, individual KDS Display, Product Category, Product, or Status.
## Installation ## Installation
Install out-of-the-box like a standard Odoo module on an Odoo 19 environment possessing POS Enterprise (`pos_enterprise`). Install out-of-the-box like a standard Odoo module on an Odoo 19 environment possessing POS Enterprise (`pos_enterprise`).
## Usage ## Usage
1. Open the Point of Sale and place orders to KDS. 1. Open the Point of Sale and place orders to the KDS.
2. Complete stages in the Preparation Display. 2. Complete stages in the Preparation Display.
3. Go to **Point of Sale > Reporting > KDS Product Analysis** or **KDS Order Analysis** to view Pivot and Graph summaries. 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.

View File

@ -1,6 +1,6 @@
{ {
'name': 'POS KDS Tracker and Report', 'name': 'POS KDS Tracker and Report',
'version': '1.0', 'version': '19.0.1.0.2',
'category': 'Sales/Point of Sale', 'category': 'Sales/Point of Sale',
'summary': 'Tracks Kitchen Display System completion times for analysis', 'summary': 'Tracks Kitchen Display System completion times for analysis',
'description': """ 'description': """
@ -14,6 +14,12 @@
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'views/pos_kds_report_views.xml', 'views/pos_kds_report_views.xml',
], ],
'assets': {
'pos_preparation_display.assets': [
'pos_kds_report/static/src/app/models/pos_preparation_state.js',
'pos_kds_report/static/src/app/services/preparation_display_service.js',
],
},
'installable': True, 'installable': True,
'application': False, 'application': False,
'auto_install': False, 'auto_install': False,

View File

@ -1,2 +1,3 @@
from . import pos_kds_report from . import pos_kds_report
from . import pos_prep_state from . import pos_prep_state
from . import pos_prep_display

View File

@ -0,0 +1,32 @@
from odoo import models
class PosPrepDisplay(models.Model):
_inherit = 'pos.prep.display'
def _notify(self, *notifications, private=True) -> None:
new_notifications = []
if isinstance(notifications[0], str):
if len(notifications) == 2:
name, message = notifications
if name == 'CHANGE_STATE_STAGE' and isinstance(message, dict):
stages = message.get('pdis_state_stages', [])
for stage_data in stages:
state = self.env['pos.prep.state'].browse(stage_data['id'])
if state.exists():
stage_data['kds_accumulated_time'] = state.kds_accumulated_time
new_notifications = notifications
else:
for item in notifications:
if isinstance(item, tuple) and len(item) == 2:
name, message = item
if name == 'CHANGE_STATE_STAGE' and isinstance(message, dict):
stages = message.get('pdis_state_stages', [])
for stage_data in stages:
state = self.env['pos.prep.state'].browse(stage_data['id'])
if state.exists():
stage_data['kds_accumulated_time'] = state.kds_accumulated_time
new_notifications.append((name, message))
else:
new_notifications.append(item)
super()._notify(*new_notifications, private=private)

View File

@ -8,6 +8,15 @@ _logger = logging.getLogger(__name__)
class PosPreparationState(models.Model): class PosPreparationState(models.Model):
_inherit = 'pos.prep.state' _inherit = 'pos.prep.state'
kds_accumulated_time = fields.Integer(string="KDS Accumulated Time", default=0)
@api.model
def _load_pos_preparation_data_fields(self):
fields_list = super()._load_pos_preparation_data_fields()
if fields_list:
fields_list.append('kds_accumulated_time')
return fields_list
def _update_kds_report(self, pdis_state, old_last_stage_change=None): def _update_kds_report(self, pdis_state, old_last_stage_change=None):
""" """
Synchronous KDS report update. Synchronous KDS report update.
@ -69,6 +78,7 @@ class PosPreparationState(models.Model):
], limit=1) ], limit=1)
if line_report: if line_report:
line_report.write({'state': 'in_prep'}) line_report.write({'state': 'in_prep'})
pdis_state.kds_accumulated_time = line_report.preparation_time
order_report = KdsOrderReport.search([ order_report = KdsOrderReport.search([
('pos_order_id', '=', order_id), ('pos_order_id', '=', order_id),

View File

@ -0,0 +1,11 @@
import { PosPreparationState } from "@pos_enterprise/app/models/pos_preparation_state";
import { patch } from "@web/core/utils/patch";
import { computeDurationSinceDate } from "@pos_enterprise/app/utils/utils";
patch(PosPreparationState.prototype, {
computeDuration() {
const baseDuration = computeDurationSinceDate(this.write_date);
const accumulatedMinutes = Math.floor((this.kds_accumulated_time || 0) / 60);
return baseDuration + accumulatedMinutes;
},
});

View File

@ -0,0 +1,16 @@
import { PrepDisplay } from "@pos_enterprise/app/services/preparation_display_service";
import { patch } from "@web/core/utils/patch";
patch(PrepDisplay.prototype, {
async setup() {
await super.setup(...arguments);
this.onNotified("CHANGE_STATE_STAGE", (data) => {
for (const stage of data["pdis_state_stages"]) {
const state = this.data.models["pos.prep.state"].get(stage.id);
if (state && stage.kds_accumulated_time !== undefined) {
state.kds_accumulated_time = stage.kds_accumulated_time;
}
}
});
}
});