Implement the initial Kitchen Display System (KDS) module, introducing order line tracking, product configuration, and dedicated kanban and tree views with custom styling.
This commit is contained in:
parent
2358e2e0ff
commit
7241758204
1
addons/dine360_kds/__init__.py
Normal file
1
addons/dine360_kds/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import models
|
||||
30
addons/dine360_kds/__manifest__.py
Normal file
30
addons/dine360_kds/__manifest__.py
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
'name': 'Dine360 Kitchen Display System',
|
||||
'version': '1.0',
|
||||
'category': 'Sales/Point of Sale',
|
||||
'summary': 'Dedicated KDS for Restaurant Kitchen',
|
||||
'description': """
|
||||
Professional Kitchen Display System:
|
||||
- Real-time order tracking
|
||||
- Preparation status management
|
||||
- Cooking time tracking
|
||||
- Kanban dashboard for chefs
|
||||
- Floor/Table based organization
|
||||
""",
|
||||
'author': 'Dine360',
|
||||
'depends': ['point_of_sale', 'pos_restaurant', 'dine360_restaurant'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/pos_order_line_views.xml',
|
||||
'views/product_views.xml',
|
||||
'views/kds_menus.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'dine360_kds/static/src/css/kds_style.css',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
2
addons/dine360_kds/models/__init__.py
Normal file
2
addons/dine360_kds/models/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from . import pos_order_line
|
||||
from . import product
|
||||
46
addons/dine360_kds/models/pos_order_line.py
Normal file
46
addons/dine360_kds/models/pos_order_line.py
Normal file
@ -0,0 +1,46 @@
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
class PosOrderLine(models.Model):
|
||||
_inherit = 'pos.order.line'
|
||||
|
||||
preparation_status = fields.Selection([
|
||||
('waiting', 'Waiting'),
|
||||
('preparing', 'Preparing'),
|
||||
('ready', 'Ready'),
|
||||
('served', 'Served'),
|
||||
('cancelled', 'Cancelled')
|
||||
], string='Preparation Status', default='waiting', tracking=True)
|
||||
|
||||
color = fields.Integer(string='Color', default=0)
|
||||
preparation_time_start = fields.Datetime(string='Start Time')
|
||||
preparation_time_end = fields.Datetime(string='Ready Time')
|
||||
cooking_time = fields.Integer(string='Cooking Time (min)', compute='_compute_cooking_time', store=True)
|
||||
|
||||
table_id = fields.Many2one('restaurant.table', related='order_id.table_id', string='Table', store=True)
|
||||
floor_id = fields.Many2one('restaurant.floor', related='order_id.table_id.floor_id', string='Floor', store=True)
|
||||
|
||||
@api.depends('preparation_time_start', 'preparation_time_end')
|
||||
def _compute_cooking_time(self):
|
||||
for line in self:
|
||||
if line.preparation_time_start and line.preparation_time_end:
|
||||
diff = line.preparation_time_end - line.preparation_time_start
|
||||
line.cooking_time = int(diff.total_seconds() / 60)
|
||||
else:
|
||||
line.cooking_time = 0
|
||||
|
||||
def action_start_preparing(self):
|
||||
self.write({
|
||||
'preparation_status': 'preparing',
|
||||
'preparation_time_start': fields.Datetime.now()
|
||||
})
|
||||
|
||||
def action_mark_ready(self):
|
||||
self.write({
|
||||
'preparation_status': 'ready',
|
||||
'preparation_time_end': fields.Datetime.now()
|
||||
})
|
||||
|
||||
def action_mark_served(self):
|
||||
self.write({
|
||||
'preparation_status': 'served'
|
||||
})
|
||||
10
addons/dine360_kds/models/product.py
Normal file
10
addons/dine360_kds/models/product.py
Normal file
@ -0,0 +1,10 @@
|
||||
from odoo import models, fields
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = 'product.template'
|
||||
|
||||
is_kitchen_item = fields.Boolean(
|
||||
string='Show in KDS',
|
||||
default=True,
|
||||
help="If checked, this product will appear in the Kitchen Display System when ordered."
|
||||
)
|
||||
4
addons/dine360_kds/security/ir.model.access.csv
Normal file
4
addons/dine360_kds/security/ir.model.access.csv
Normal file
@ -0,0 +1,4 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_kds_order_line_kitchen,pos.order.line.kitchen,point_of_sale.model_pos_order_line,dine360_restaurant.group_restaurant_kitchen,1,1,0,0
|
||||
access_kds_order_line_manager,pos.order.line.manager,point_of_sale.model_pos_order_line,dine360_restaurant.group_restaurant_manager,1,1,1,1
|
||||
access_kds_order_line_user,pos.order.line.user,point_of_sale.model_pos_order_line,base.group_user,1,1,1,0
|
||||
|
49
addons/dine360_kds/static/src/css/kds_style.css
Normal file
49
addons/dine360_kds/static/src/css/kds_style.css
Normal file
@ -0,0 +1,49 @@
|
||||
.role_kitchen_card {
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.role_kitchen_card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
.o_kanban_group {
|
||||
background-color: #f4f7fa !important;
|
||||
border-radius: 15px !important;
|
||||
margin: 10px !important;
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.o_kanban_header {
|
||||
background: transparent !important;
|
||||
padding: 15px 10px !important;
|
||||
}
|
||||
|
||||
.o_kanban_header_title {
|
||||
font-weight: 700 !important;
|
||||
text-transform: uppercase !important;
|
||||
letter-spacing: 1px !important;
|
||||
}
|
||||
|
||||
/* Specific colors for status columns */
|
||||
.o_kanban_group[data-id="waiting"] .o_kanban_header_title {
|
||||
color: #f0ad4e;
|
||||
}
|
||||
|
||||
.o_kanban_group[data-id="preparing"] .o_kanban_header_title {
|
||||
color: #5bc0de;
|
||||
}
|
||||
|
||||
.o_kanban_group[data-id="ready"] .o_kanban_header_title {
|
||||
color: #5cb85c;
|
||||
}
|
||||
|
||||
.o_kanban_group[data-id="served"] .o_kanban_header_title {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.badge.rounded-pill {
|
||||
padding: 0.5em 1em;
|
||||
font-weight: 600;
|
||||
}
|
||||
14
addons/dine360_kds/views/kds_menus.xml
Normal file
14
addons/dine360_kds/views/kds_menus.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<odoo>
|
||||
<menuitem id="menu_kds_root"
|
||||
name="Kitchen (KDS)"
|
||||
web_icon="point_of_sale,static/src/img/icon.png"
|
||||
groups="base.group_user"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem id="menu_kds_orders"
|
||||
name="Preparation Orders"
|
||||
parent="menu_kds_root"
|
||||
action="action_kds_dashboard"
|
||||
groups="base.group_user"
|
||||
sequence="10"/>
|
||||
</odoo>
|
||||
138
addons/dine360_kds/views/pos_order_line_views.xml
Normal file
138
addons/dine360_kds/views/pos_order_line_views.xml
Normal file
@ -0,0 +1,138 @@
|
||||
<odoo>
|
||||
<!-- KDS Kanban View -->
|
||||
<record id="view_pos_order_line_kds_kanban" model="ir.ui.view">
|
||||
<field name="name">pos.order.line.kds.kanban</field>
|
||||
<field name="model">pos.order.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban default_group_by="preparation_status" create="false" class="o_kanban_small_column o_kanban_project_tasks" sample="1">
|
||||
<field name="preparation_status"/>
|
||||
<field name="color"/>
|
||||
<field name="product_id"/>
|
||||
<field name="qty"/>
|
||||
<field name="order_id"/>
|
||||
<field name="table_id"/>
|
||||
<field name="floor_id"/>
|
||||
<field name="customer_note"/>
|
||||
<field name="create_date"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="oe_kanban_global_click role_kitchen_card {{'oe_kanban_color_' + kanban_getcolor(record.color.raw_value)}} shadow-sm border-0 mb-3" style="border-radius: 12px; border-left: 5px solid #fecd4f !important;">
|
||||
<div class="oe_kanban_content p-3">
|
||||
<div class="o_kanban_record_top mb-2">
|
||||
<div class="o_kanban_record_headings">
|
||||
<strong class="o_kanban_record_title" style="font-size: 1.2rem; color: #171422;">
|
||||
<field name="qty"/> x <field name="product_id"/>
|
||||
</strong>
|
||||
</div>
|
||||
<div class="ms-auto h5 mb-0">
|
||||
<span class="badge rounded-pill bg-light text-dark border">
|
||||
<i class="fa fa-cutlery me-1"/> <field name="table_id"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<t t-if="record.customer_note.raw_value">
|
||||
<div class="alert alert-warning py-2 px-3 mb-3 border-0" style="background: rgba(254, 205, 79, 0.1); border-radius: 8px;">
|
||||
<i class="fa fa-sticky-note-o me-2"/> <strong>Note:</strong> <field name="customer_note"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<div class="o_kanban_record_body small text-muted mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span><i class="fa fa-clock-o me-1"/> <field name="create_date" widget="relative_time"/></span>
|
||||
<span class="text-uppercase fw-bold" style="font-size: 0.7rem;"><field name="floor_id"/></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="o_kanban_record_bottom border-top pt-3 mt-2">
|
||||
<div class="oe_kanban_bottom_left">
|
||||
<field name="preparation_status" widget="label_selection" options="{'classes': {'waiting': 'warning', 'preparing': 'info', 'ready': 'success', 'served': 'muted'}}"/>
|
||||
</div>
|
||||
<div class="oe_kanban_bottom_right">
|
||||
<button t-if="record.preparation_status.raw_value == 'waiting'"
|
||||
name="action_start_preparing" type="object"
|
||||
class="btn btn-sm btn-primary px-3 shadow-sm" style="background: #171422; border: none; border-radius: 8px;">
|
||||
Start Cooking
|
||||
</button>
|
||||
<button t-if="record.preparation_status.raw_value == 'preparing'"
|
||||
name="action_mark_ready" type="object"
|
||||
class="btn btn-sm btn-success px-3 shadow-sm" style="background: #28a745; border: none; border-radius: 8px;">
|
||||
Mark Ready
|
||||
</button>
|
||||
<button t-if="record.preparation_status.raw_value == 'ready'"
|
||||
name="action_mark_served" type="object"
|
||||
class="btn btn-sm btn-outline-dark px-3 shadow-sm" style="border-radius: 8px;">
|
||||
Served
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- KDS Tree View -->
|
||||
<record id="view_pos_order_line_kds_tree" model="ir.ui.view">
|
||||
<field name="name">pos.order.line.kds.tree</field>
|
||||
<field name="model">pos.order.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Kitchen Orders" create="false" edit="false" decoration-info="preparation_status == 'preparing'" decoration-success="preparation_status == 'ready'" decoration-muted="preparation_status == 'served'">
|
||||
<field name="order_id"/>
|
||||
<field name="floor_id"/>
|
||||
<field name="table_id"/>
|
||||
<field name="product_id"/>
|
||||
<field name="qty"/>
|
||||
<field name="customer_note" string="Notes"/>
|
||||
<field name="preparation_status" widget="badge" decoration-info="preparation_status == 'preparing'" decoration-success="preparation_status == 'ready'" decoration-warning="preparation_status == 'waiting'"/>
|
||||
<field name="create_date" string="Ordered At" widget="remaining_days"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- KDS Search View -->
|
||||
<record id="view_pos_order_line_kds_search" model="ir.ui.view">
|
||||
<field name="name">pos.order.line.kds.search</field>
|
||||
<field name="model">pos.order.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Kitchen Orders Search">
|
||||
<field name="order_id"/>
|
||||
<field name="product_id"/>
|
||||
<field name="table_id"/>
|
||||
<field name="floor_id"/>
|
||||
<separator/>
|
||||
<filter string="Waiting" name="waiting" domain="[('preparation_status', '=', 'waiting')]"/>
|
||||
<filter string="Preparing" name="preparing" domain="[('preparation_status', '=', 'preparing')]"/>
|
||||
<filter string="Ready" name="ready" domain="[('preparation_status', '=', 'ready')]"/>
|
||||
<filter string="In Progress" name="in_progress" domain="[('preparation_status', 'in', ['waiting', 'preparing'])]"/>
|
||||
<separator/>
|
||||
<filter string="Today" name="today" domain="[('create_date', '>=', context_today().strftime('%Y-%m-%d 00:00:00'))]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Status" name="group_status" context="{'group_by': 'preparation_status'}"/>
|
||||
<filter string="Table" name="group_table" context="{'group_by': 'table_id'}"/>
|
||||
<filter string="Floor" name="group_floor" context="{'group_by': 'floor_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Window Action -->
|
||||
<record id="action_kds_dashboard" model="ir.actions.act_window">
|
||||
<field name="name">Kitchen Display System</field>
|
||||
<field name="res_model">pos.order.line</field>
|
||||
<field name="view_mode">kanban,tree,form</field>
|
||||
<field name="domain">[('product_id.is_kitchen_item', '=', True)]</field>
|
||||
<field name="search_view_id" ref="view_pos_order_line_kds_search"/>
|
||||
<field name="context">{'search_default_in_progress': 1, 'search_default_group_status': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Welcome to the Kitchen!
|
||||
</p>
|
||||
<p>
|
||||
Orders sent from the POS will appear here for preparation.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
13
addons/dine360_kds/views/product_views.xml
Normal file
13
addons/dine360_kds/views/product_views.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="product_template_form_view_kds" model="ir.ui.view">
|
||||
<field name="name">product.template.form.kds</field>
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="product.product_template_only_form_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='categ_id']" position="after">
|
||||
<field name="is_kitchen_item"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user