13 changed files with 856 additions and 280 deletions
@ -0,0 +1,26 @@ |
|||||
|
from odoo import models, fields, api |
||||
|
from odoo.exceptions import ValidationError |
||||
|
import re |
||||
|
|
||||
|
|
||||
|
class ProductProduct(models.Model): |
||||
|
_inherit = 'product.product' |
||||
|
|
||||
|
prepair_time_minutes = fields.Float( |
||||
|
string='Preparation Time (MM:SS)', |
||||
|
digits=(12, 2), |
||||
|
help="Enter time in MM:SS format (e.g., 20:12 for 20 minutes 12 seconds)" |
||||
|
) |
||||
|
|
||||
|
@api.onchange('prepair_time_minutes') |
||||
|
def _onchange_prepair_time(self): |
||||
|
if isinstance(self.prepair_time_minutes, str): |
||||
|
try: |
||||
|
# Validate format MM:SS |
||||
|
if not re.match(r'^\d{1,3}:[0-5][0-9]$', self.prepair_time_minutes): |
||||
|
raise ValidationError("Please enter time in MM:SS format (e.g., 20:12)") |
||||
|
|
||||
|
minutes, seconds = map(int, self.prepair_time_minutes.split(':')) |
||||
|
self.prepair_time_minutes = minutes + (seconds / 60.0) |
||||
|
except (ValueError, AttributeError): |
||||
|
raise ValidationError("Invalid time format. Please use MM:SS (e.g., 20:12)") |
@ -1,144 +1,304 @@ |
|||||
/** @odoo-module */ |
/** @odoo-module */ |
||||
import { registry } from "@web/core/registry"; |
import { registry } from "@web/core/registry"; |
||||
const { Component, onWillStart, useState, onMounted } = owl; |
const { Component, onMounted, onWillUnmount, useState } = owl; |
||||
import { useService } from "@web/core/utils/hooks"; |
import { useService } from "@web/core/utils/hooks"; |
||||
|
|
||||
|
class KitchenScreenDashboard extends Component { |
||||
class kitchen_screen_dashboard extends Component { |
setup() { |
||||
setup(env) { |
|
||||
super.setup(); |
super.setup(); |
||||
this.busService = this.env.services.bus_service; |
|
||||
this.busService.addChannel("pos_order_created"); |
// Services
|
||||
onWillStart(() => { |
|
||||
this.busService.subscribe('notification', this.onPosOrderCreation.bind(this));}) |
|
||||
this.action = useService("action"); |
this.action = useService("action"); |
||||
this.rpc = this.env.services.rpc; |
this.rpc = this.env.services.rpc; |
||||
this.action = useService("action"); |
|
||||
this.orm = useService("orm"); |
this.orm = useService("orm"); |
||||
var self=this |
this.busService = useService("bus_service"); |
||||
|
|
||||
|
// Method binding
|
||||
|
this.getCurrentShopId = this.getCurrentShopId.bind(this); |
||||
|
this.loadOrders = this.loadOrders.bind(this); |
||||
|
this.startCountdown = this.startCountdown.bind(this); |
||||
|
this.updateCountdownState = this.updateCountdownState.bind(this); |
||||
|
this.onPosOrderCreation = this.onPosOrderCreation.bind(this); |
||||
|
this.accept_order = this.accept_order.bind(this); |
||||
|
this.done_order = this.done_order.bind(this); |
||||
|
this.cancel_order = this.cancel_order.bind(this); |
||||
|
this.accept_order_line = this.accept_order_line.bind(this); |
||||
|
this.forceRefresh = this.forceRefresh.bind(this); |
||||
|
|
||||
|
// Stage change methods
|
||||
|
this.ready_stage = (e) => this.state.stages = 'ready'; |
||||
|
this.waiting_stage = (e) => this.state.stages = 'waiting'; |
||||
|
this.draft_stage = (e) => this.state.stages = 'draft'; |
||||
|
|
||||
|
// Initialization
|
||||
|
this.currentShopId = this.getCurrentShopId(); |
||||
|
this.channel = `pos_order_created_${this.currentShopId}`; |
||||
|
this.countdownIntervals = {}; |
||||
|
|
||||
|
// State management
|
||||
this.state = useState({ |
this.state = useState({ |
||||
order_details: [], |
order_details: [], |
||||
shop_id:[], |
shop_id: this.currentShopId, |
||||
stages: 'draft', |
stages: 'draft', |
||||
draft_count:[], |
draft_count: 0, |
||||
waiting_count:[], |
waiting_count: 0, |
||||
ready_count:[], |
ready_count: 0, |
||||
lines:[] |
lines: [], |
||||
|
prepare_times: [], |
||||
|
countdowns: {}, |
||||
|
isLoading: false |
||||
|
}); |
||||
|
|
||||
|
// Component lifecycle
|
||||
|
onMounted(() => { |
||||
|
this.busService.addChannel(this.channel); |
||||
|
this.busService.subscribe('notification', this.onPosOrderCreation); |
||||
|
this.loadOrders(); |
||||
|
|
||||
|
this.autoRefreshInterval = setInterval(() => { |
||||
|
this.loadOrders(); |
||||
|
}, 30000); |
||||
}); |
}); |
||||
this.orm.call("pos.session", "search_read", [[ |
|
||||
["state", "=", "opened"] // Get only open sessions
|
onWillUnmount(() => { |
||||
]]).then(function(sessions) { |
this.busService.deleteChannel(this.channel); |
||||
if (sessions.length > 0) { |
this.busService.unsubscribe('notification', this.onPosOrderCreation); |
||||
self.state.session_ids = sessions.map(session => session.id); // Store session IDs in state
|
if (this.autoRefreshInterval) { |
||||
} else { |
clearInterval(this.autoRefreshInterval); |
||||
self.state.session_ids = [] |
} |
||||
} |
Object.values(this.countdownIntervals).forEach(interval => { |
||||
|
clearInterval(interval); |
||||
|
}); |
||||
|
this.countdownIntervals = {}; |
||||
}); |
}); |
||||
|
} |
||||
|
|
||||
var session_shop_id; |
getCurrentShopId() { |
||||
// //if refreshing the page then the last passed context (shop id)
|
let session_shop_id; |
||||
// //save to the session storage
|
if (this.props.action?.context?.default_shop_id) { |
||||
if (this.props.action.context.default_shop_id) { |
|
||||
sessionStorage.setItem('shop_id', this.props.action.context.default_shop_id); |
sessionStorage.setItem('shop_id', this.props.action.context.default_shop_id); |
||||
this.shop_id = this.props.action.context.default_shop_id; |
session_shop_id = this.props.action.context.default_shop_id; |
||||
session_shop_id = sessionStorage.getItem('shop_id'); |
|
||||
} else { |
} else { |
||||
session_shop_id = sessionStorage.getItem('shop_id'); |
session_shop_id = sessionStorage.getItem('shop_id'); |
||||
this.shop_id = parseInt(session_shop_id, 10);; |
|
||||
} |
} |
||||
self.orm.call("pos.order", "get_details", ["", self.shop_id,""]).then(function(result) { |
return parseInt(session_shop_id, 10) || 0; |
||||
self.state.order_details = result['orders'].filter(order => order.session_id && self.state.session_ids.includes(order.session_id[0])); |
|
||||
self.state.lines = result['order_lines'] |
|
||||
self.state.shop_id=self.shop_id |
|
||||
self.state.draft_count=self.state.order_details.filter((order) => order.order_status=='draft' && order.config_id[0]==self.state.shop_id).length |
|
||||
self.state.waiting_count=self.state.order_details.filter((order) => order.order_status=='waiting' && order.config_id[0]==self.state.shop_id).length |
|
||||
self.state.ready_count=self.state.order_details.filter((order) => order.order_status=='ready' && order.config_id[0]==self.state.shop_id).length |
|
||||
}); |
|
||||
} |
} |
||||
|
|
||||
//Calling the onPosOrderCreation when an order is created or edited on the backend and return the notification
|
async loadOrders() { |
||||
onPosOrderCreation(message){ |
if (this.state.isLoading) return; |
||||
var self=this |
|
||||
if(message.message == "pos_order_created" && message.res_model == "pos.order"){ |
try { |
||||
self.orm.call("pos.order", "get_details", ["", self.shop_id,""]).then(function(result) { |
this.state.isLoading = true; |
||||
self.state.order_details = result['orders'].filter(order => order.session_id && self.state.session_ids.includes(order.session_id[0])); |
const result = await this.orm.call("pos.order", "get_details", [this.currentShopId]); |
||||
self.state.lines = result['order_lines'] |
|
||||
self.state.shop_id=self.shop_id |
this.state.order_details = result.orders || []; |
||||
self.state.draft_count=self.state.order_details.filter((order) => order.order_status=='draft' && order.config_id[0]==self.state.shop_id).length |
this.state.lines = result.order_lines || []; |
||||
self.state.waiting_count=self.state.order_details.filter((order) => order.order_status=='waiting' && order.config_id[0]==self.state.shop_id).length |
|
||||
self.state.ready_count=self.state.order_details.filter((order) => order.order_status=='ready' && order.config_id[0]==self.state.shop_id).length |
const activeOrders = this.state.order_details.filter(order => { |
||||
|
const configMatch = Array.isArray(order.config_id) ? |
||||
|
order.config_id[0] === this.currentShopId : |
||||
|
order.config_id === this.currentShopId; |
||||
|
return configMatch && order.order_status !== 'cancel' && order.state !== 'cancel'; |
||||
|
}); |
||||
|
const productIds = [...new Set(this.state.lines.map(line => line.product_id[0]))]; |
||||
|
if (productIds.length) { |
||||
|
const overTimes = await this.orm.call( |
||||
|
"product.product", |
||||
|
"search_read", |
||||
|
[[["id", "in", productIds]], ["id", "prepair_time_minutes"]] |
||||
|
); |
||||
|
|
||||
|
this.state.prepare_times = overTimes.map(item => ({ |
||||
|
...item, |
||||
|
prepare_time: !item.prepair_time_minutes ? "00:00:00" : |
||||
|
typeof item.prepair_time_minutes === 'number' ? |
||||
|
parseFloat(item.prepair_time_minutes.toFixed(2)) : |
||||
|
item.prepair_time_minutes |
||||
|
})); |
||||
|
} |
||||
|
this.state.draft_count = activeOrders.filter(o => o.order_status === 'draft').length; |
||||
|
this.state.waiting_count = activeOrders.filter(o => o.order_status === 'waiting').length; |
||||
|
this.state.ready_count = activeOrders.filter(o => o.order_status === 'ready').length; |
||||
|
|
||||
|
activeOrders.forEach(order => { |
||||
|
if (order.order_status === 'waiting' && order.avg_prepare_time) { |
||||
|
if (!this.countdownIntervals[order.id]) { |
||||
|
this.startCountdown(order.id, order.avg_prepare_time); |
||||
|
} |
||||
|
} else if (order.order_status === 'ready') { |
||||
|
this.updateCountdownState(order.id, 0, true); |
||||
|
if (this.countdownIntervals[order.id]) { |
||||
|
clearInterval(this.countdownIntervals[order.id]); |
||||
|
delete this.countdownIntervals[order.id]; |
||||
|
} |
||||
|
} |
||||
}); |
}); |
||||
|
} catch (error) { |
||||
|
console.error("Error loading orders:", error); |
||||
|
} finally { |
||||
|
this.state.isLoading = false; |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
// cancel the order from the kitchen
|
async startCountdown(orderId, timeString,config_id) { |
||||
cancel_order(e) { |
if (this.countdownIntervals[orderId]) { |
||||
var input_id = $("#" + e.target.id).val(); |
clearInterval(this.countdownIntervals[orderId]); |
||||
this.orm.call("pos.order", "order_progress_cancel", [Number(input_id)]) |
} |
||||
var current_order = this.state.order_details.filter((order) => order.id==input_id) |
|
||||
if(current_order){ |
const [minutes, seconds] = timeString.toFixed(2).split('.').map(Number); |
||||
current_order[0].order_status = 'cancel' |
let totalSeconds = minutes * 60 + seconds; |
||||
} |
|
||||
} |
this.updateCountdownState(orderId, totalSeconds, false); |
||||
// accept the order from the kitchen
|
|
||||
accept_order(e) { |
this.countdownIntervals[orderId] = setInterval(async () => { |
||||
var input_id = $("#" + e.target.id).val(); |
totalSeconds--; |
||||
ScrollReveal().reveal("#" + e.target.id, { |
this.updateCountdownState(orderId, totalSeconds, false); |
||||
delay: 1000, |
if (totalSeconds <= 0) { |
||||
duration: 2000, |
try { |
||||
opacity: 0, |
|
||||
distance: "50%", |
let orderData = await this.orm.call( |
||||
origin: "top", |
'kitchen.screen', |
||||
reset: true, |
'search_read', |
||||
interval: 600, |
[ |
||||
}); |
[["pos_config_id", "=", config_id[0]]], |
||||
var self=this |
["is_preparation_complete"] |
||||
this.orm.call("pos.order", "order_progress_draft", [Number(input_id)]) |
] |
||||
var current_order = this.state.order_details.filter((order) => order.id==input_id) |
); |
||||
if(current_order){ |
clearInterval(this.countdownIntervals[orderId]); |
||||
current_order[0].order_status = 'waiting' |
delete this.countdownIntervals[orderId]; |
||||
} |
this.updateCountdownState(orderId, 0, true); |
||||
|
if (orderData[0].is_preparation_complete === true){ |
||||
|
this.done_order({ target: { value: orderId.toString() } }); |
||||
|
} |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error("Error fetching order data:", error); |
||||
|
// Handle error appropriately
|
||||
|
} |
||||
|
} |
||||
|
}, 1000); |
||||
} |
} |
||||
// set the stage is ready to see the completed stage orders
|
|
||||
ready_stage(e) { |
updateCountdownState(orderId, totalSeconds, isCompleted = false) { |
||||
var self = this; |
const minutes = Math.floor(totalSeconds / 60); |
||||
self.state.stages = 'ready'; |
const seconds = totalSeconds % 60; |
||||
|
this.state.countdowns = { |
||||
|
...this.state.countdowns, |
||||
|
[orderId]: { |
||||
|
minutes, |
||||
|
seconds, |
||||
|
isCompleted |
||||
|
} |
||||
|
}; |
||||
} |
} |
||||
//set the stage is waiting to see the ready stage orders
|
|
||||
waiting_stage(e) { |
onPosOrderCreation(message) { |
||||
var self = this; |
if (!message || message.config_id !== this.currentShopId) { |
||||
self.state.stages = 'waiting'; |
return; |
||||
|
} |
||||
|
|
||||
|
const relevantMessages = [ |
||||
|
'pos_order_created', |
||||
|
'pos_order_updated', |
||||
|
'pos_order_paid', |
||||
|
'pos_order_accepted', |
||||
|
'pos_order_cancelled', |
||||
|
'pos_order_completed', |
||||
|
'pos_order_line_updated' |
||||
|
]; |
||||
|
|
||||
|
if ((message.res_model === "pos.order" || message.res_model === "pos.order.line") && |
||||
|
relevantMessages.includes(message.message)) { |
||||
|
this.loadOrders(); |
||||
|
} |
||||
} |
} |
||||
//set the stage is draft to see the cooking stage orders
|
|
||||
draft_stage(e) { |
async accept_order(e) { |
||||
var self = this; |
const orderId = Number(e.target.value); |
||||
self.state.stages = 'draft'; |
try { |
||||
|
await this.orm.call("pos.order", "order_progress_draft", [orderId]); |
||||
|
|
||||
|
const order = this.state.order_details.find(o => o.id === orderId); |
||||
|
if (order) { |
||||
|
order.order_status = 'waiting'; |
||||
|
if (order.avg_prepare_time) { |
||||
|
this.startCountdown(orderId, order.avg_prepare_time, order.config_id); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
setTimeout(() => this.loadOrders(), 500); |
||||
|
} catch (error) { |
||||
|
console.error("Error accepting order:", error); |
||||
|
} |
||||
} |
} |
||||
// change the status of the order from the kitchen
|
|
||||
done_order(e) { |
async done_order(e) { |
||||
var self = this; |
const orderId = Number(e.target.value); |
||||
var input_id = $("#" + e.target.id).val(); |
try { |
||||
this.orm.call("pos.order", "order_progress_change", [Number(input_id)]) |
await this.orm.call("pos.order", "order_progress_change", [orderId]); |
||||
var current_order = this.state.order_details.filter((order) => order.id==input_id) |
|
||||
if(current_order){ |
const order = this.state.order_details.find(o => o.id === orderId); |
||||
current_order[0].order_status = 'ready' |
if (order) { |
||||
} |
order.order_status = 'ready'; |
||||
|
this.updateCountdownState(orderId, 0, true); |
||||
|
if (this.countdownIntervals[orderId]) { |
||||
|
clearInterval(this.countdownIntervals[orderId]); |
||||
|
delete this.countdownIntervals[orderId]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
setTimeout(() => this.loadOrders(), 500); |
||||
|
} catch (error) { |
||||
|
console.error("Error completing order:", error); |
||||
|
} |
||||
} |
} |
||||
// change the status of the product from the kitchen
|
|
||||
accept_order_line(e) { |
async cancel_order(e) { |
||||
var input_id = $("#" + e.target.id).val(); |
const orderId = Number(e.target.value); |
||||
this.orm.call("pos.order.line", "order_progress_change", [Number(input_id)]) |
try { |
||||
var current_order_line=this.state.lines.filter((order_line) => order_line.id==input_id) |
await this.orm.call("pos.order", "order_progress_cancel", [orderId]); |
||||
if (current_order_line){ |
|
||||
if (current_order_line[0].order_status == 'ready'){ |
const order = this.state.order_details.find(o => o.id === orderId); |
||||
current_order_line[0].order_status = 'waiting' |
if (order) { |
||||
|
order.order_status = 'cancel'; |
||||
} |
} |
||||
else{ |
|
||||
current_order_line[0].order_status = 'ready' |
setTimeout(() => this.loadOrders(), 500); |
||||
|
} catch (error) { |
||||
|
console.error("Error cancelling order:", error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async accept_order_line(e) { |
||||
|
const lineId = Number(e.target.value); |
||||
|
try { |
||||
|
await this.orm.call("pos.order.line", "order_progress_change", [lineId]); |
||||
|
|
||||
|
const line = this.state.lines.find(l => l.id === lineId); |
||||
|
if (line) { |
||||
|
line.order_status = line.order_status === 'ready' ? 'waiting' : 'ready'; |
||||
} |
} |
||||
|
|
||||
|
setTimeout(() => this.loadOrders(), 500); |
||||
|
} catch (error) { |
||||
|
console.error("Error updating order line:", error); |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
|
get filteredOrders() { |
||||
|
return this.state.order_details.filter(order => { |
||||
|
const configMatch = Array.isArray(order.config_id) ? |
||||
|
order.config_id[0] === this.currentShopId : |
||||
|
order.config_id === this.currentShopId; |
||||
|
const stageMatch = order.order_status === this.state.stages; |
||||
|
return configMatch && stageMatch && order.order_status !== 'cancel'; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
forceRefresh() { |
||||
|
this.loadOrders(); |
||||
|
} |
||||
} |
} |
||||
kitchen_screen_dashboard.template = 'KitchenCustomDashBoard'; |
|
||||
registry.category("actions").add("kitchen_custom_dashboard_tags", kitchen_screen_dashboard); |
KitchenScreenDashboard.template = 'KitchenCustomDashBoard'; |
||||
|
registry.category("actions").add("kitchen_custom_dashboard_tags", KitchenScreenDashboard); |
@ -0,0 +1,13 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<odoo> |
||||
|
<record id="product_product_view_form_inherit" model="ir.ui.view"> |
||||
|
<field name="name">product.product.form.inherit</field> |
||||
|
<field name="model">product.product</field> |
||||
|
<field name="inherit_id" ref="product.product_normal_form_view"/> |
||||
|
<field name="arch" type="xml"> |
||||
|
<xpath expr="//field[@name='to_weight']" position="after"> |
||||
|
<field name="prepair_time_minutes" widget="float_time"/> |
||||
|
</xpath> |
||||
|
</field> |
||||
|
</record> |
||||
|
</odoo> |
Loading…
Reference in new issue