first commit
This commit is contained in:
commit
aa4a78f481
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
__pycache__/
|
||||||
|
*.log
|
||||||
|
.vscode/
|
||||||
18
README.md
Normal file
18
README.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
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.
|
||||||
1
__init__.py
Normal file
1
__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
21
__manifest__.py
Normal file
21
__manifest__.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
'name': 'POS KDS Tracker and Report',
|
||||||
|
'version': '1.0',
|
||||||
|
'category': 'Sales/Point of Sale',
|
||||||
|
'summary': 'Tracks Kitchen Display System completion times for analysis',
|
||||||
|
'description': """
|
||||||
|
This module tracks and persists the completion times of KDS orders and lines.
|
||||||
|
It enables detailed historical reporting on the average time to complete an order,
|
||||||
|
product, or product category across POS Shops and specific Kitchen Display Systems.
|
||||||
|
""",
|
||||||
|
'author': 'Your Name',
|
||||||
|
'depends': ['point_of_sale', 'pos_enterprise'],
|
||||||
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'views/pos_kds_report_views.xml',
|
||||||
|
],
|
||||||
|
'installable': True,
|
||||||
|
'application': False,
|
||||||
|
'auto_install': False,
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
2
models/__init__.py
Normal file
2
models/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from . import pos_kds_report
|
||||||
|
from . import pos_prep_state
|
||||||
61
models/pos_kds_report.py
Normal file
61
models/pos_kds_report.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
from odoo import fields, models, api
|
||||||
|
|
||||||
|
|
||||||
|
class PosKdsReportLine(models.Model):
|
||||||
|
_name = 'pos.kds.report.line'
|
||||||
|
_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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
@api.depends('product_id')
|
||||||
|
def _compute_pos_category(self):
|
||||||
|
for record in self:
|
||||||
|
record.pos_category_id = record.product_id.pos_categ_ids[:1] if record.product_id.pos_categ_ids else False
|
||||||
|
|
||||||
|
@api.depends('pos_order_id')
|
||||||
|
def _compute_pos_config(self):
|
||||||
|
for record in self:
|
||||||
|
record.pos_config_id = record.pos_order_id.session_id.config_id
|
||||||
|
|
||||||
|
def name_get(self):
|
||||||
|
result = []
|
||||||
|
for record in self:
|
||||||
|
name = f"{record.pos_order_id.name} - {record.product_id.name}"
|
||||||
|
result.append((record.id, name))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class PosKdsReportOrder(models.Model):
|
||||||
|
_name = 'pos.kds.report.order'
|
||||||
|
_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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
@api.depends('pos_order_id')
|
||||||
|
def _compute_pos_config(self):
|
||||||
|
for record in self:
|
||||||
|
record.pos_config_id = record.pos_order_id.session_id.config_id
|
||||||
|
|
||||||
|
def name_get(self):
|
||||||
|
result = []
|
||||||
|
for record in self:
|
||||||
|
name = f"{record.pos_order_id.name} ({record.prep_display_id.name})"
|
||||||
|
result.append((record.id, name))
|
||||||
|
return result
|
||||||
103
models/pos_prep_state.py
Normal file
103
models/pos_prep_state.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
from odoo import models
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
is_reset = False
|
||||||
|
if pdis_state.stage_id.is_stage_position(0) and pdis_state.todo:
|
||||||
|
is_reset = True
|
||||||
|
|
||||||
|
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:
|
||||||
|
prep_time = max(0, order_line.preparation_time)
|
||||||
|
svc_time = max(0, order_line.service_time)
|
||||||
|
comp_time = prep_time + svc_time
|
||||||
|
|
||||||
|
vals = {
|
||||||
|
'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,
|
||||||
|
'preparation_time': prep_time,
|
||||||
|
'service_time': svc_time,
|
||||||
|
'completion_time': comp_time,
|
||||||
|
}
|
||||||
|
if line_report:
|
||||||
|
line_report.write(vals)
|
||||||
|
else:
|
||||||
|
self.env['pos.kds.report.line'].create(vals)
|
||||||
|
|
||||||
|
# Update order-level report
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
order_vals = {
|
||||||
|
'pos_order_id': order_line.order_id.id,
|
||||||
|
'prep_display_id': prep_display.id,
|
||||||
|
'completion_time': max_comp_time,
|
||||||
|
}
|
||||||
|
if order_report:
|
||||||
|
order_report.write(order_vals)
|
||||||
|
else:
|
||||||
|
self.env['pos.kds.report.order'].create(order_vals)
|
||||||
|
|
||||||
|
elif is_reset:
|
||||||
|
if line_report:
|
||||||
|
line_report.unlink()
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
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})
|
||||||
|
|
||||||
|
def _record_status_change_prep_time(self, pdis_state):
|
||||||
|
super()._record_status_change_prep_time(pdis_state)
|
||||||
|
self._update_kds_report(pdis_state)
|
||||||
|
|
||||||
|
def _record_stage_change_prep_time(self, pdis_state, old_last_stage_change, prep_order_completion_time):
|
||||||
|
super()._record_stage_change_prep_time(pdis_state, old_last_stage_change, prep_order_completion_time)
|
||||||
|
self._update_kds_report(pdis_state)
|
||||||
3
security/ir.model.access.csv
Normal file
3
security/ir.model.access.csv
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_pos_kds_report_line,pos.kds.report.line,model_pos_kds_report_line,point_of_sale.group_pos_user,1,1,1,1
|
||||||
|
access_pos_kds_report_order,pos.kds.report.order,model_pos_kds_report_order,point_of_sale.group_pos_user,1,1,1,1
|
||||||
|
131
views/pos_kds_report_views.xml
Normal file
131
views/pos_kds_report_views.xml
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<!-- VIEWS FOR KDS PRODUCT REPORT (pos.kds.report.line) -->
|
||||||
|
|
||||||
|
<record id="view_pos_kds_report_line_pivot" model="ir.ui.view">
|
||||||
|
<field name="name">pos.kds.report.line.pivot</field>
|
||||||
|
<field name="model">pos.kds.report.line</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<pivot string="KDS Product Analysis" display_quantity="1" sample="1">
|
||||||
|
<field name="pos_category_id" type="row"/>
|
||||||
|
<field name="product_id" type="row"/>
|
||||||
|
<field name="completion_time" type="measure" operator="avg"/>
|
||||||
|
<field name="preparation_time" type="measure" operator="avg"/>
|
||||||
|
<field name="service_time" type="measure" operator="avg"/>
|
||||||
|
</pivot>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_pos_kds_report_line_graph" model="ir.ui.view">
|
||||||
|
<field name="name">pos.kds.report.line.graph</field>
|
||||||
|
<field name="model">pos.kds.report.line</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<graph string="KDS Product Analysis" type="bar" sample="1">
|
||||||
|
<field name="pos_category_id" type="row"/>
|
||||||
|
<field name="product_id" type="row"/>
|
||||||
|
<field name="completion_time" type="measure" operator="avg"/>
|
||||||
|
</graph>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_pos_kds_report_line_search" model="ir.ui.view">
|
||||||
|
<field name="name">pos.kds.report.line.search</field>
|
||||||
|
<field name="model">pos.kds.report.line</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="KDS Product Analysis">
|
||||||
|
<field name="pos_order_id"/>
|
||||||
|
<field name="product_id"/>
|
||||||
|
<field name="pos_category_id"/>
|
||||||
|
<field name="prep_display_id"/>
|
||||||
|
<field name="pos_config_id"/>
|
||||||
|
|
||||||
|
<filter string="My KDS Displays" name="my_kds" domain="[('prep_display_id', '!=', False)]"/>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<filter string="Date" name="group_by_date" domain="[]" context="{'group_by':'completion_datetime:day'}"/>
|
||||||
|
<filter string="Product Category" name="group_by_category" domain="[]" context="{'group_by':'pos_category_id'}"/>
|
||||||
|
<filter string="Product" name="group_by_product" domain="[]" context="{'group_by':'product_id'}"/>
|
||||||
|
<filter string="KDS Display" name="group_by_kds" domain="[]" context="{'group_by':'prep_display_id'}"/>
|
||||||
|
<filter string="POS Shop" name="group_by_pos" domain="[]" context="{'group_by':'pos_config_id'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_pos_kds_report_line" model="ir.actions.act_window">
|
||||||
|
<field name="name">KDS Product Analysis</field>
|
||||||
|
<field name="res_model">pos.kds.report.line</field>
|
||||||
|
<field name="view_mode">pivot,graph</field>
|
||||||
|
<field name="search_view_id" ref="view_pos_kds_report_line_search"/>
|
||||||
|
<field name="context">{'search_default_group_by_kds': 1}</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- VIEWS FOR KDS ORDER REPORT (pos.kds.report.order) -->
|
||||||
|
|
||||||
|
<record id="view_pos_kds_report_order_pivot" model="ir.ui.view">
|
||||||
|
<field name="name">pos.kds.report.order.pivot</field>
|
||||||
|
<field name="model">pos.kds.report.order</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<pivot string="KDS Order Analysis" display_quantity="1" sample="1">
|
||||||
|
<field name="completion_datetime" type="row" interval="day"/>
|
||||||
|
<field name="prep_display_id" type="row"/>
|
||||||
|
<field name="completion_time" type="measure" operator="avg"/>
|
||||||
|
</pivot>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_pos_kds_report_order_graph" model="ir.ui.view">
|
||||||
|
<field name="name">pos.kds.report.order.graph</field>
|
||||||
|
<field name="model">pos.kds.report.order</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<graph string="KDS Order Analysis" type="line" sample="1">
|
||||||
|
<field name="completion_datetime" type="row" interval="day"/>
|
||||||
|
<field name="completion_time" type="measure" operator="avg"/>
|
||||||
|
</graph>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_pos_kds_report_order_search" model="ir.ui.view">
|
||||||
|
<field name="name">pos.kds.report.order.search</field>
|
||||||
|
<field name="model">pos.kds.report.order</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="KDS Order Analysis">
|
||||||
|
<field name="pos_order_id"/>
|
||||||
|
<field name="prep_display_id"/>
|
||||||
|
<field name="pos_config_id"/>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<filter string="Date" name="group_by_date" domain="[]" context="{'group_by':'completion_datetime:day'}"/>
|
||||||
|
<filter string="KDS Display" name="group_by_kds" domain="[]" context="{'group_by':'prep_display_id'}"/>
|
||||||
|
<filter string="POS Shop" name="group_by_pos" domain="[]" context="{'group_by':'pos_config_id'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_pos_kds_report_order" model="ir.actions.act_window">
|
||||||
|
<field name="name">KDS Order Analysis</field>
|
||||||
|
<field name="res_model">pos.kds.report.order</field>
|
||||||
|
<field name="view_mode">pivot,graph</field>
|
||||||
|
<field name="search_view_id" ref="view_pos_kds_report_order_search"/>
|
||||||
|
<field name="context">{'search_default_group_by_kds': 1}</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- MENUS -->
|
||||||
|
|
||||||
|
<menuitem id="menu_pos_kds_report_line"
|
||||||
|
name="KDS Product Analysis"
|
||||||
|
parent="point_of_sale.menu_point_rep"
|
||||||
|
action="action_pos_kds_report_line"
|
||||||
|
sequence="50"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_pos_kds_report_order"
|
||||||
|
name="KDS Order Analysis"
|
||||||
|
parent="point_of_sale.menu_point_rep"
|
||||||
|
action="action_pos_kds_report_order"
|
||||||
|
sequence="55"/>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
Loading…
Reference in New Issue
Block a user