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 */ |
|||
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"; |
|||
|
|||
|
|||
class kitchen_screen_dashboard extends Component { |
|||
setup(env) { |
|||
class KitchenScreenDashboard extends Component { |
|||
setup() { |
|||
super.setup(); |
|||
this.busService = this.env.services.bus_service; |
|||
this.busService.addChannel("pos_order_created"); |
|||
onWillStart(() => { |
|||
this.busService.subscribe('notification', this.onPosOrderCreation.bind(this));}) |
|||
|
|||
// Services
|
|||
this.action = useService("action"); |
|||
this.rpc = this.env.services.rpc; |
|||
this.action = useService("action"); |
|||
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({ |
|||
order_details: [], |
|||
shop_id:[], |
|||
shop_id: this.currentShopId, |
|||
stages: 'draft', |
|||
draft_count:[], |
|||
waiting_count:[], |
|||
ready_count:[], |
|||
lines:[] |
|||
draft_count: 0, |
|||
waiting_count: 0, |
|||
ready_count: 0, |
|||
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
|
|||
]]).then(function(sessions) { |
|||
if (sessions.length > 0) { |
|||
self.state.session_ids = sessions.map(session => session.id); // Store session IDs in state
|
|||
} else { |
|||
self.state.session_ids = [] |
|||
} |
|||
|
|||
onWillUnmount(() => { |
|||
this.busService.deleteChannel(this.channel); |
|||
this.busService.unsubscribe('notification', this.onPosOrderCreation); |
|||
if (this.autoRefreshInterval) { |
|||
clearInterval(this.autoRefreshInterval); |
|||
} |
|||
Object.values(this.countdownIntervals).forEach(interval => { |
|||
clearInterval(interval); |
|||
}); |
|||
this.countdownIntervals = {}; |
|||
}); |
|||
} |
|||
|
|||
var session_shop_id; |
|||
// //if refreshing the page then the last passed context (shop id)
|
|||
// //save to the session storage
|
|||
if (this.props.action.context.default_shop_id) { |
|||
getCurrentShopId() { |
|||
let session_shop_id; |
|||
if (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 = sessionStorage.getItem('shop_id'); |
|||
session_shop_id = this.props.action.context.default_shop_id; |
|||
} else { |
|||
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) { |
|||
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 |
|||
}); |
|||
return parseInt(session_shop_id, 10) || 0; |
|||
} |
|||
|
|||
//Calling the onPosOrderCreation when an order is created or edited on the backend and return the notification
|
|||
onPosOrderCreation(message){ |
|||
var self=this |
|||
if(message.message == "pos_order_created" && message.res_model == "pos.order"){ |
|||
self.orm.call("pos.order", "get_details", ["", self.shop_id,""]).then(function(result) { |
|||
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 |
|||
async loadOrders() { |
|||
if (this.state.isLoading) return; |
|||
|
|||
try { |
|||
this.state.isLoading = true; |
|||
const result = await this.orm.call("pos.order", "get_details", [this.currentShopId]); |
|||
|
|||
this.state.order_details = result.orders || []; |
|||
this.state.lines = result.order_lines || []; |
|||
|
|||
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
|
|||
cancel_order(e) { |
|||
var input_id = $("#" + e.target.id).val(); |
|||
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){ |
|||
current_order[0].order_status = 'cancel' |
|||
} |
|||
} |
|||
// accept the order from the kitchen
|
|||
accept_order(e) { |
|||
var input_id = $("#" + e.target.id).val(); |
|||
ScrollReveal().reveal("#" + e.target.id, { |
|||
delay: 1000, |
|||
duration: 2000, |
|||
opacity: 0, |
|||
distance: "50%", |
|||
origin: "top", |
|||
reset: true, |
|||
interval: 600, |
|||
}); |
|||
var self=this |
|||
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){ |
|||
current_order[0].order_status = 'waiting' |
|||
} |
|||
async startCountdown(orderId, timeString,config_id) { |
|||
if (this.countdownIntervals[orderId]) { |
|||
clearInterval(this.countdownIntervals[orderId]); |
|||
} |
|||
|
|||
const [minutes, seconds] = timeString.toFixed(2).split('.').map(Number); |
|||
let totalSeconds = minutes * 60 + seconds; |
|||
|
|||
this.updateCountdownState(orderId, totalSeconds, false); |
|||
|
|||
this.countdownIntervals[orderId] = setInterval(async () => { |
|||
totalSeconds--; |
|||
this.updateCountdownState(orderId, totalSeconds, false); |
|||
if (totalSeconds <= 0) { |
|||
try { |
|||
|
|||
let orderData = await this.orm.call( |
|||
'kitchen.screen', |
|||
'search_read', |
|||
[ |
|||
[["pos_config_id", "=", config_id[0]]], |
|||
["is_preparation_complete"] |
|||
] |
|||
); |
|||
clearInterval(this.countdownIntervals[orderId]); |
|||
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) { |
|||
var self = this; |
|||
self.state.stages = 'ready'; |
|||
|
|||
updateCountdownState(orderId, totalSeconds, isCompleted = false) { |
|||
const minutes = Math.floor(totalSeconds / 60); |
|||
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) { |
|||
var self = this; |
|||
self.state.stages = 'waiting'; |
|||
|
|||
onPosOrderCreation(message) { |
|||
if (!message || message.config_id !== this.currentShopId) { |
|||
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) { |
|||
var self = this; |
|||
self.state.stages = 'draft'; |
|||
|
|||
async accept_order(e) { |
|||
const orderId = Number(e.target.value); |
|||
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) { |
|||
var self = this; |
|||
var input_id = $("#" + e.target.id).val(); |
|||
this.orm.call("pos.order", "order_progress_change", [Number(input_id)]) |
|||
var current_order = this.state.order_details.filter((order) => order.id==input_id) |
|||
if(current_order){ |
|||
current_order[0].order_status = 'ready' |
|||
} |
|||
|
|||
async done_order(e) { |
|||
const orderId = Number(e.target.value); |
|||
try { |
|||
await this.orm.call("pos.order", "order_progress_change", [orderId]); |
|||
|
|||
const order = this.state.order_details.find(o => o.id === orderId); |
|||
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) { |
|||
var input_id = $("#" + e.target.id).val(); |
|||
this.orm.call("pos.order.line", "order_progress_change", [Number(input_id)]) |
|||
var current_order_line=this.state.lines.filter((order_line) => order_line.id==input_id) |
|||
if (current_order_line){ |
|||
if (current_order_line[0].order_status == 'ready'){ |
|||
current_order_line[0].order_status = 'waiting' |
|||
|
|||
async cancel_order(e) { |
|||
const orderId = Number(e.target.value); |
|||
try { |
|||
await this.orm.call("pos.order", "order_progress_cancel", [orderId]); |
|||
|
|||
const order = this.state.order_details.find(o => o.id === orderId); |
|||
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