diff --git a/README.md b/README.md index 85284cd..2999a32 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,19 @@ ## 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. +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 -- Records the state of the Kitchen Display System or Preparation Display permanently. -- Generates reports showing the average time to complete an order. -- Generates reports showing the average time to complete certain products or product categories. -- Allows filtering and grouping globally for a POS Shop or individually per KDS. +- **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. ## 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 KDS. +1. Open the Point of Sale and place orders to the KDS. 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. diff --git a/__manifest__.py b/__manifest__.py index f65e2b0..1101d14 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'POS KDS Tracker and Report', - 'version': '1.0', + 'version': '19.0.1.0.2', 'category': 'Sales/Point of Sale', 'summary': 'Tracks Kitchen Display System completion times for analysis', 'description': """ @@ -14,6 +14,12 @@ 'security/ir.model.access.csv', '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, 'application': False, 'auto_install': False, diff --git a/models/__init__.py b/models/__init__.py index e1262ca..4964927 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -1,2 +1,3 @@ from . import pos_kds_report from . import pos_prep_state +from . import pos_prep_display diff --git a/models/pos_prep_display.py b/models/pos_prep_display.py new file mode 100644 index 0000000..145227d --- /dev/null +++ b/models/pos_prep_display.py @@ -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) diff --git a/models/pos_prep_state.py b/models/pos_prep_state.py index 25b4130..9158504 100644 --- a/models/pos_prep_state.py +++ b/models/pos_prep_state.py @@ -8,6 +8,15 @@ _logger = logging.getLogger(__name__) class PosPreparationState(models.Model): _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): """ Synchronous KDS report update. @@ -69,6 +78,7 @@ class PosPreparationState(models.Model): ], limit=1) if line_report: line_report.write({'state': 'in_prep'}) + pdis_state.kds_accumulated_time = line_report.preparation_time order_report = KdsOrderReport.search([ ('pos_order_id', '=', order_id), diff --git a/static/src/app/models/pos_preparation_state.js b/static/src/app/models/pos_preparation_state.js new file mode 100644 index 0000000..930ea77 --- /dev/null +++ b/static/src/app/models/pos_preparation_state.js @@ -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; + }, +}); diff --git a/static/src/app/services/preparation_display_service.js b/static/src/app/services/preparation_display_service.js new file mode 100644 index 0000000..6dcf955 --- /dev/null +++ b/static/src/app/services/preparation_display_service.js @@ -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; + } + } + }); + } +});