chore: import enterprise addons 19.0 bulk snapshot

This commit is contained in:
1b-agent committed 2026-09-16 10:13:08 +00:00
commit ca4af030d3
39951 files changed
+15570005

No files matched your search

@@ -0,0 +1,13 @@
import { ClosePosPopup } from "@point_of_sale/app/components/popups/closing_popup/closing_popup";
import { patch } from "@web/core/utils/patch";
patch(ClosePosPopup.prototype, {
/**
* @override
*/
async confirm() {
await this.pos.updateStoreStatus();
localStorage.removeItem("toggle_state_" + this.pos.config.id);
return super.confirm();
},
});
@@ -0,0 +1,12 @@
import { patch } from "@web/core/utils/patch";
import { OpeningControlPopup } from "@point_of_sale/app/components/popups/opening_control_popup/opening_control_popup";
patch(OpeningControlPopup.prototype, {
/**
* @override
*/
async confirm() {
await super.confirm();
await this.pos.updateStoreStatus(true);
},
});
@@ -0,0 +1,166 @@
import { Dialog } from "@web/core/dialog/dialog";
import { Component } from "@odoo/owl";
import { usePos } from "@point_of_sale/app/hooks/pos_hook";
import { _t } from "@web/core/l10n/translation";
import { getTime } from "@pos_urban_piper/utils";
export class orderInfoPopup extends Component {
static components = { Dialog };
static template = "pos_urban_piper.orderInfoPopup";
static props = {
order: Object,
order_status: Object,
close: Function,
};
setup() {
this.pos = usePos();
this.deliveryJson = JSON.parse(this.props.order.delivery_json || "{}");
this.extPlatform = this.deliveryJson?.order?.details?.ext_platforms?.[0];
this.store = this.deliveryJson?.order?.store;
this.deliveryRiderJson = JSON.parse(this.props.order.delivery_rider_json || "{}");
this.payment_option_display = {
prepaid: _t("Prepaid"),
payment_gateway: _t("Payment Gateway"),
cash: _t("Cash"),
card_on_delivery: _t("Card on Delivery"),
paytm: _t("Paytm"),
wallet_credit: _t("Wallet Credit"),
simpl: _t("Simpl (Deferred Payment)"),
aggregator: _t("Aggregator (Handled by Platform)"),
};
const partner = this.props.order?.partner_id;
this.cardsData = [
{
title: _t("Delivery Person"),
icon: "fa-motorcycle",
visible: Boolean(this.props.order.delivery_rider_json),
fields: [
{
label: _t("Name"),
value: this.deliveryRiderJson?.delivery_person_details?.name,
},
{
label: _t("Phone"),
value: this.deliveryRiderJson?.delivery_person_details?.phone,
link: `tel:${this.deliveryRiderJson?.delivery_person_details?.phone}`,
},
{
label: _t("Status"),
value: this.deliveryRiderJson?.status_updates?.[
this.deliveryRiderJson?.status_updates.length - 1
]?.status,
},
],
},
{
title: _t("Order Info"),
icon: "fa-bookmark",
visible: true,
fields: [
{
label: _t("Status"),
value: this.props.order_status[this.props.order.delivery_status],
},
{ label: _t("Fulfilment Mode"), value: this.getOrderDetails().fulfilmentMode },
{
label: _t("Channel"),
value: `${this.props.order.delivery_provider_id?.name} - ${
this.getOrderDetails().channelOtp
}`,
},
{ label: _t("Outlet"), value: this.getOrderDetails().outletName },
{ label: _t("Payment Mode"), value: this.getOrderDetails().paymentMode },
{
label: _t("Order Time"),
value: getTime(this.deliveryJson?.order?.details?.created),
},
{
label: _t("Delivery Time/Time-Slot"),
value: getTime(this.deliveryJson?.order?.details?.delivery_datetime),
},
...(this.getOrderDetails().talabatCode
? [{ label: _t("Talabat Code"), value: this.getOrderDetails().talabatCode }]
: []),
...(this.getOrderDetails().talabatShortCode
? [
{
label: _t("Talabat Short Code"),
value: this.getOrderDetails().talabatShortCode,
},
]
: []),
...(this.getOrderDetails().hungerstationCode
? [
{
label: _t("HungerStation Code"),
value: this.getOrderDetails().hungerstationCode,
},
]
: []),
{ label: _t("Order ID"), value: this.props.order.delivery_identifier },
...(this.getOrderDetails().orderOtp
? [{ label: _t("Order OTP"), value: this.getOrderDetails().orderOtp }]
: []),
],
},
...(partner
? [
{
title: _t("Customer Info"),
icon: "fa-user",
visible: true,
fields: [
{
label: _t("Customer Name"),
value: partner.name,
},
{
label: _t("Delivery Address"),
value: `${partner.street || ""} ${partner.city || ""}`.trim(),
},
{
label: _t("Customer Phone"),
value: partner.phone,
link: `tel:${partner.phone}`,
},
{
label: _t("Customer Email"),
value: partner.email,
link: `mailto:${partner.email}`,
},
],
},
]
: []),
];
}
onClose() {
this.props.close();
}
getOrderDetails() {
const orderDetails = {
channelOtp: this.extPlatform?.id,
orderOtp: this.extPlatform?.extras?.order_otp,
fulfilmentMode: this.extPlatform?.delivery_type,
outletName: this.store?.name,
paymentMode:
this.deliveryJson?.order?.payment
?.map(
(payment) => this.payment_option_display[payment.option] || payment.option
)
?.join(", ") || _t("Not Specified"),
};
const deliveryProvider = this.props?.order?.delivery_provider_id?.technical_name;
if (deliveryProvider === "talabat") {
orderDetails["talabatCode"] = this.extPlatform?.extras?.talabat_code;
orderDetails["talabatShortCode"] = this.extPlatform?.extras?.talabat_shortcode;
}
if (deliveryProvider === "hungerstation") {
orderDetails["hungerstationCode"] = this.extPlatform?.extras?.hungerstation_code;
}
return orderDetails;
}
}
@@ -0,0 +1,38 @@
<templates id="template" xml:space="preserve">
<t t-name="pos_urban_piper.orderInfoPopup">
<Dialog size="'md'" title.translate="Details">
<t t-foreach="this.cardsData" t-as="card" t-key="card.title">
<t t-if="card.visible">
<div class="card rounded mt-3">
<div class="card-header d-flex align-items-center">
<i t-att-class="`fa ${card.icon}`" aria-hidden="true"/>
<h5 class="ms-2 mb-0"><t t-esc="card.title"/></h5>
</div>
<div class="card-body">
<div class="row">
<t t-foreach="card.fields" t-as="field" t-key="field.label">
<div t-if="field.value" class="col-md-6 mb-2">
<div class="d-flex flex-column">
<div class="text-muted fs-7"><t t-esc="field.label"/></div>
<div>
<t t-if="field.link">
<a t-att-href="field.link"><t t-esc="field.value"/></a>
</t>
<t t-else="">
<t t-esc="field.value"/>
</t>
</div>
</div>
</div>
</t>
</div>
</div>
</div>
</t>
</t>
<t t-set-slot="footer">
<button class="button btn btn-primary" t-on-click="onClose">Close</button>
</t>
</Dialog>
</t>
</templates>
@@ -0,0 +1,42 @@
import { _t } from "@web/core/l10n/translation";
import { patch } from "@web/core/utils/patch";
import { ProductInfoPopup } from "@point_of_sale/app/components/popups/product_info_popup/product_info_popup";
patch(ProductInfoPopup.prototype, {
get product() {
return this.props.productTemplate;
},
get availableForFoodDelivery() {
return this.product.isAvailableForFoodDelivery(this.pos.config.id);
},
get showFoodDeliveryAvailability() {
return (
this.pos.config.module_pos_urban_piper &&
this.allowProductEdition &&
this.product._synced_on_urbanpiper
);
},
async switchFoodDeliveryAvailability() {
this.pos.env.services.ui.block();
try {
const response = await this.pos.data.call(
"product.template",
"toggle_product_food_delivery_availability",
[this.product.id, this.pos.config.id],
{
context: {
from_pos_ui: true,
},
}
);
if (response.status !== "success") {
this.pos.notification.add(response.error, { type: "danger" });
}
} catch {
this.pos.notification.add(_t("Failed to update food delivery availability."), {
type: "danger",
});
}
this.pos.env.services.ui.unblock();
},
});
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_urban_piper.ProductInfoPopup" t-inherit="point_of_sale.ProductInfoPopup" t-inherit-mode="extension">
<xpath expr="//div[hasclass('section-inventory')]" position="before">
<div t-if="this.showFoodDeliveryAvailability" class="section-food-delivery-availability mt-3 mb-4 pb-4 border-bottom text-start d-flex align-items-center">
<h3 class="section-title">Online Food Delivery:</h3>
<div class="section-foog-delivery-availability-body d-flex ms-auto">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" t-att-checked="this.availableForFoodDelivery"
t-on-click="() => this.switchFoodDeliveryAvailability()"/>
</div>
<span>
<t t-if="this.availableForFoodDelivery">Available</t>
<t t-else="">Not Available</t>
</span>
</div>
</div>
</xpath>
</t>
</templates>
@@ -0,0 +1,46 @@
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { Component } from "@odoo/owl";
import { usePos } from "@point_of_sale/app/hooks/pos_hook";
import { useService } from "@web/core/utils/hooks";
export class DeliveryButton extends Component {
static template = "pos_urban_piper.DeliveryButton";
static components = {
Dropdown,
DropdownItem,
};
static props = {};
setup() {
this.pos = usePos();
this.ui = useService("ui");
}
async handleToggle(providerCode) {
const toggleStatus = !this.pos.enabledProviders[providerCode];
this.pos.enabledProviders[providerCode] = toggleStatus;
await this.pos.saveProviderState(this.pos.enabledProviders);
await this.pos.updateStoreStatus(toggleStatus, providerCode);
}
goToOrders(deliveryProvider, upState, filter = "ONGOING") {
const stateOverride = {
search: {
fieldName: "DELIVERYPROVIDER",
searchTerm: deliveryProvider,
},
filter: filter,
};
if (this.pos.router.state.current == "TicketScreen") {
this.env.services.ui.block();
this.pos.ticket_screen_mobile_pane = "left";
setTimeout(() => {
this.pos.navigate("TicketScreen", { stateOverride: stateOverride, upState });
this.env.services.ui.unblock();
}, 300);
return;
}
this.pos.navigate("TicketScreen", { stateOverride: stateOverride, upState });
}
}
@@ -0,0 +1,28 @@
.button-hover:hover {
background-color: lightgray;
}
.delivery-icon-container {
position: relative;
.new-order-badge {
width: 16px;
height: 16px;
top: -10px;
right: -12px;
font-size: 11px;
color: white;
background: #e6294f;
}
}
.provider-image {
height: 50px;
width: 50px;
border-radius: 7px;
}
.urbanpiper-dropdown-container {
overflow: auto;
max-height: 20pc;
}
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_urban_piper.DeliveryButton">
<Dropdown>
<div class="delivery-icon-container me-2">
<div class="d-flex align-items-center justify-content-center position-absolute rounded-circle new-order-badge">
<t t-out="this.pos.total_new_order"/>
</div>
<i class="oi oi-food-delivery ms-1 fs-2"/>
</div>
<t t-set-slot="content">
<t t-if="this.pos.delivery_providers">
<t t-set="delivery_order_count" t-value="this.pos.delivery_order_count.urbanpiper"/>
<div class="urbanpiper-dropdown-container">
<t t-foreach="this.pos.delivery_providers" t-as="provider" t-key="provider.id">
<DropdownItem>
<div class="d-flex">
<div t-attf-class="px-2 {{ this.ui.isSmall ? 'w-100' : 'container-fluid'}}">
<div t-attf-class="d-flex h-100 {{ this.ui.isSmall ? 'justify-content-between' : 'gap-2'}}">
<img class="me-1 provider-image" t-att-src="'/web/image?model=pos.delivery.provider&amp;field=image_128&amp;id=' + provider.id"/>
<div class="button-hover" t-on-click="() => this.goToOrders(provider.name, 'NEW')">
<div class="d-flex flex-column">
<span><b>New</b></span>
<span class="text-center">
<t t-out="delivery_order_count[provider['code']].awaiting"/>
</span>
</div>
</div>
<div class="button-hover" t-on-click="() => this.goToOrders(provider.name, 'ONGOING')">
<div class="d-flex flex-column h-100 w-100">
<span><b>Ongoing</b></span>
<span class="text-center">
<t t-out="delivery_order_count[provider['code']].preparing"/>
</span>
</div>
</div>
<div class="button-hover" t-on-click="() => this.goToOrders(provider.name, 'DONE', 'SYNCED')">
<div class="d-flex flex-column">
<span><b>Done</b></span>
<span class="text-center">
<t t-out="delivery_order_count[provider['code']].done"/>
</span>
</div>
</div>
<div class="d-flex justify-content-center align-items-center">
<div class="form-switch d-flex justify-content-end align-items-center" onclick="event.stopPropagation()">
<input type="checkbox" class="form-check-input" t-att-checked="this.pos.enabledProviders[provider.code]" t-on-click="() => this.handleToggle(provider.code)" />
</div>
</div>
</div>
</div>
</div>
</DropdownItem>
</t>
</div>
</t>
<t t-else="">
<span class="m-2 d-flex justify-content-center"><strong>There are no active delivery providers(s).</strong></span>
</t>
</t>
</Dropdown>
</t>
</templates>
@@ -0,0 +1,17 @@
import { ProductTemplate } from "@point_of_sale/app/models/product_template";
import { patch } from "@web/core/utils/patch";
patch(ProductTemplate.prototype, {
isAvailableForFoodDelivery(configId) {
return this.raw.urbanpiper_pos_config_ids.includes(configId);
},
setFoodDeliveryAvailability(status, configId) {
let availableConfigIds = [...this.raw.urbanpiper_pos_config_ids, configId];
if (!status) {
availableConfigIds = availableConfigIds.filter((id) => id !== configId);
}
this.update({
urbanpiper_pos_config_ids: availableConfigIds,
});
},
});
@@ -0,0 +1,14 @@
import { Chrome } from "@point_of_sale/app/pos_app";
import { patch } from "@web/core/utils/patch";
patch(Chrome.prototype, {
setup() {
super.setup(...arguments);
this.pos.data.connectWebSocket("DELIVERY_ORDER_COUNT", async (order_id) => {
await this.pos._fetchUrbanpiperOrderCount(order_id);
});
this.pos.data.connectWebSocket("STORE_ACTION", async (data) => {
await this.pos._fetchStoreAction(data);
});
},
});
@@ -0,0 +1,29 @@
import { PosData } from "@point_of_sale/app/services/data_service";
import { patch } from "@web/core/utils/patch";
patch(PosData.prototype, {
/**
* @override
*/
async preLoadData(data) {
const loadData = await super.preLoadData(data);
// FIXME: bad implementation, data can be undefined if there is nothing in indexedDB
if (!data["pos.config"] || !Object.keys(data["pos.config"]).length) {
return loadData;
}
const config = this.models["pos.config"].getFirst();
if (!config.module_pos_urban_piper || !config.urbanpiper_store_identifier) {
return loadData;
}
if (loadData["pos.order"]) {
loadData["pos.order"] = loadData["pos.order"].filter((o) => !o.delivery_identifier);
}
if (loadData["pos.order.line"]) {
loadData["pos.order.line"] = loadData["pos.order.line"].filter(
(ol) => ol.order_id && !ol.order_id.delivery_identifier
);
}
return loadData;
},
});
@@ -0,0 +1,288 @@
import { patch } from "@web/core/utils/patch";
import { PosStore } from "@point_of_sale/app/services/pos_store";
import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { _t } from "@web/core/l10n/translation";
import { Domain } from "@web/core/domain";
patch(PosStore.prototype, {
/**
* @override
*/
async setup() {
await super.setup(...arguments);
this.delivery_order_count = {};
this.enabledProviders = {};
// Init provider states from other sources
if (this.config.module_pos_urban_piper) {
await this.initProviderStatus();
}
this.data.connectWebSocket("URBAN_PIPER_PROVIDER_STATES", async (data) => {
this.enabledProviders = data;
});
if (this.config.module_pos_urban_piper && this.config.urbanpiper_store_identifier) {
await this._fetchUrbanpiperOrderCount(false);
}
this.isSoundPlaying = false;
this.data.connectWebSocket(
"PRODUCT_UP_STATUS_CHANGED",
this.notifyFoodDeliveryStatus.bind(this)
);
},
notifyFoodDeliveryStatus(data) {
const { product_ids, status } = data;
const products = this.models["product.template"].filter((product) =>
product_ids.includes(product.id)
);
if (!products.length) {
return;
}
products.forEach((product) => {
product.setFoodDeliveryAvailability(status, this.config.id);
this.notification.add(
_t(
"%s is %s online food delivery for all platform in this locations.",
product.name,
status ? _t("enabled") : _t("disabled")
),
{
type: status ? "success" : "warning",
sticky: false,
}
);
});
},
async saveProviderState(newStates = {}) {
this.enabledProviders = await this.data.call(
"pos.config",
"set_urban_piper_provider_states",
[this.config.id, JSON.stringify(newStates)]
);
},
async getProviderState() {
const provideState = await this.data.call("pos.config", "get_urban_piper_provider_states", [
this.config.id,
]);
return provideState || {};
},
async initProviderStatus() {
// If certain providers are not yet in the status cache, we create it and set it to true.
let changed = false;
this.enabledProviders = await this.getProviderState();
for (const provider of this.config.urbanpiper_delivery_provider_ids) {
const name = provider.technical_name;
const currentValue = this.enabledProviders[name];
const newValue = currentValue === undefined ? true : this.enabledProviders[name];
if (currentValue === undefined) {
changed = true; // Initialize provider state to true
}
this.enabledProviders[name] = newValue;
}
if (changed) {
await this.saveProviderState(this.enabledProviders);
}
},
async updateStoreStatus(status = false, providerName = false) {
if (this.config.module_pos_urban_piper && this.config.urbanpiper_store_identifier) {
await this.data.call("pos.config", "update_store_status", [this.config.id, status], {
context: {
providerName: providerName,
},
});
}
},
getServerOrdersDomain() {
const base = super.getServerOrdersDomain();
if (this.config.module_pos_urban_piper && this.config.urbanpiper_store_identifier) {
return Domain.or([
base,
new Domain([
["company_id", "=", this.config.company_id.id],
["state", "=", "draft"],
["session_id", "=", this.session.id],
[
"delivery_provider_id",
"in",
this.config.urbanpiper_delivery_provider_ids.map((provider) => provider.id),
],
]),
]);
}
return base;
},
_fetchStoreAction(data) {
const params = {
type: "success",
sticky: false,
};
let message = "";
if (data.status) {
this.enabledProviders[data.platform] = data.action === "enable";
this.saveProviderState(this.enabledProviders);
}
// Prepare notification message
if (!data.status) {
params.type = "danger";
message = _t("Error occurred while updating " + data.platform + " status.");
} else if (data.action === "enable") {
message = _t(this.config.name + " is online on " + data.platform + ".");
} else if (data.action === "disable") {
message = _t(this.config.name + " is offline on " + data.platform + ".");
}
if (message) {
this.notification.add(message, params);
}
},
get notificationOptions() {
return {
type: "success",
sticky: true,
buttons: [
{
name: _t("Review Orders"),
onClick: () => {
this.closeNotificationFn?.();
const stateOverride = {
search: {
fieldName: "DELIVERYPROVIDER",
searchTerm:
this.deliveryOrderNotification?.delivery_provider_id.name,
},
filter: "ACTIVE_ORDERS",
};
this.setOrder(this.deliveryOrderNotification);
if (this.router.state.current == "TicketScreen") {
const next = this.defaultPage;
this.navigate(next.page, next.params);
setTimeout(() => {
this.navigate("TicketScreen", { stateOverride });
this.env.services.ui.unblock();
}, 300);
return;
}
return this.navigate("TicketScreen", { stateOverride });
},
},
],
onClose: () => {
if (this.isSoundPlaying) {
this.sound.stop("order-receive-tone");
this.isSoundPlaying = false;
}
},
};
},
async _fetchUrbanpiperOrderCount(order_id) {
try {
await this.getServerOrders();
} catch {
this.notification.add(_t("Order does not load from server"), {
type: "warning",
sticky: false,
});
}
const response = await this.data.call(
"pos.config",
"get_delivery_data",
[this.config.id],
{}
);
this.delivery_order_count = response.delivery_order_count;
this.delivery_providers = response.delivery_providers;
this.total_new_order = response.total_new_order;
const deliveryOrder = order_id ? this.models["pos.order"].get(order_id) : false;
if (!deliveryOrder) {
return;
}
if (deliveryOrder.delivery_status === "acknowledged" && deliveryOrder.state != "cancel") {
await this._sendDeliveryOrderForPreparation(deliveryOrder);
} else if (deliveryOrder.delivery_status === "placed") {
if (!this.isSoundPlaying) {
this.isSoundPlaying = true;
this.sound.play("order-receive-tone", { loop: true, volume: 1 });
this.deliveryOrderNotification = deliveryOrder;
this.closeNotificationFn = this.notification.add(
_t("New online order received."),
this.notificationOptions
);
}
}
},
async _sendDeliveryOrderForPreparation(deliveryOrder) {
if (
deliveryOrder.last_order_preparation_change.urbanpiper_printed ||
deliveryOrder.isFutureOrder()
) {
return;
}
let isReadyToPrint = true;
try {
isReadyToPrint = await this.data.call(
"pos.order",
"mark_urbanpiper_prep_order_as_printed",
[deliveryOrder.id]
);
} catch {
isReadyToPrint = false;
}
if (isReadyToPrint) {
await this.sendOrderInPreparationUpdateLastChange(deliveryOrder);
}
},
async goToBack() {
this.addPendingOrder([this.getOrder().id]);
await this.syncAllOrders();
this.navigate("TicketScreen");
if (this.getOrder().delivery_status !== "placed") {
try {
await this.checkPreparationStateAndSentOrderInPreparation(this.getOrder());
} catch {
this.notification.add(_t("Error to send in preparation display."), {
type: "warning",
sticky: false,
});
}
}
},
getOrderData(order, reprint) {
let orderData = super.getOrderData(order, reprint);
if (order.delivery_provider_id) {
orderData = {
...orderData,
delivery_provider_id: {
id: order.delivery_provider_id.id,
name: order.delivery_provider_id.name,
},
order_otp: JSON.parse(order.delivery_json)?.order?.details?.ext_platforms?.[0].id,
prep_time: order.prep_time,
};
}
return orderData;
},
async onDeleteOrder(order) {
if (!order?.delivery_identifier) {
return super.onDeleteOrder(...arguments);
}
this.dialog.add(AlertDialog, {
title: _t("Online Order"),
body: _t(
"Online orders cannot be deleted. If needed, reject the order instead or contact the food delivery provider."
),
});
return false;
},
});
@@ -0,0 +1,7 @@
import { Navbar } from "@point_of_sale/app/components/navbar/navbar";
import { patch } from "@web/core/utils/patch";
import { DeliveryButton } from "@pos_urban_piper/point_of_sale_overrirde/app/delivery_button/delivery_button";
patch(Navbar, {
components: { ...Navbar.components, DeliveryButton },
});
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_urban_piper.Navbar" t-inherit="point_of_sale.Navbar" t-inherit-mode="extension">
<xpath expr="//Input[@t-if='pos.showSearchButton()']" position="after">
<DeliveryButton t-if="this.pos.config.module_pos_urban_piper and this.pos.config.urbanpiper_store_identifier"/>
</xpath>
</t>
</templates>
@@ -0,0 +1,48 @@
import { OrderDisplay } from "@point_of_sale/app/components/order_display/order_display";
import { patch } from "@web/core/utils/patch";
import { useState } from "@odoo/owl";
import { usePos } from "@point_of_sale/app/hooks/pos_hook";
patch(OrderDisplay, {
props: {
...OrderDisplay.props,
orderAcceptTime: { type: Number, optional: true },
orderPrepTime: { type: Number, optional: true },
},
});
patch(OrderDisplay.prototype, {
setup() {
super.setup(...arguments);
this.pos = usePos();
this.state = useState({ remainingTime: 0 });
this.state.remainingTime = this._computeRemainingTime();
this.interval = setInterval(() => {
this.state.remainingTime = this._computeRemainingTime();
}, 10000);
},
_computeRemainingTime() {
if (this.showTimer) {
const total_order_time =
this.props.orderAcceptTime + this.props.orderPrepTime * 60 * 1000;
return Math.max(
0,
Math.round((total_order_time - luxon.DateTime.now().ts) / (1000 * 60))
);
}
},
get showTimer() {
return this.props.orderAcceptTime && this.order?.state !== "paid";
},
changePrepTime(order, increment) {
order.prep_time = Math.max(0, order.prep_time + (increment ? 5 : -5));
},
onInputChangePrepTime(order, inputTime, event) {
order.prep_time = Math.max(0, parseInt(inputTime || 0));
event.target.value = order.prep_time;
},
});
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_urban_piper.OrderDisplay" t-inherit="point_of_sale.OrderDisplay" t-inherit-mode="extension">
<xpath expr="//div[hasclass('order-summary')]" position="before">
<div t-if="showTimer" class="ms-auto p-2 px-2">
<div class="rounded-pill p-1 px-2" t-att-class="{'text-bg-danger': state.remainingTime lt 5, 'text-bg-info text-white': state.remainingTime >= 5}">
<i class="fa fa-clock-o px-1" aria-hidden="true"/>
<span t-esc="state.remainingTime" />'
</div>
</div>
</xpath>
<xpath expr="//div[hasclass('tax-info')]" position="before">
<div t-if="this.pos.router.state.current == 'TicketScreen' and this.props.order.delivery_provider_id and this.props.order.delivery_status == 'placed'" class="d-flex align-items-center gap-2 my-2">
<t t-set="currentOrder" t-value="this.props.order"/>
<button
type="button"
class="btn btn-secondary btn-sm px-3 fs-2"
t-on-click="() => this.changePrepTime(currentOrder, false)">
-
</button>
<input
type="number"
class="form-control text-center form-control-sm fs-2"
t-att-value="this.props.order.prep_time"
t-on-blur="(event) => this.onInputChangePrepTime(currentOrder, event.target.value, event)"/>
<button
type="button"
class="increment-btn btn btn-secondary m-0 btn-sm px-3 fs-2"
t-on-click="() => this.changePrepTime(currentOrder, true)">
+
</button>
<span class="fs-3">Min(s)</span>
</div>
</xpath>
</t>
</templates>
@@ -0,0 +1,17 @@
import { patch } from "@web/core/utils/patch";
import { PaymentScreen } from "@point_of_sale/app/screens/payment_screen/payment_screen";
patch(PaymentScreen.prototype, {
/**
* @override
*/
setup() {
super.setup();
if (this.currentOrder?.isDeliveryRefundOrder) {
this.payment_methods_from_config = [
...this.payment_methods_from_config,
...this.pos.config.urbanpiper_payment_methods_ids,
];
}
},
});
@@ -0,0 +1,12 @@
import { _t } from "@web/core/l10n/translation";
import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen";
import { patch } from "@web/core/utils/patch";
import { BackButton } from "@point_of_sale/app/screens/product_screen/action_pad/back_button/back_button";
patch(ProductScreen, {
components: {
...ProductScreen.components,
BackButton,
},
back: _t("Back"),
});
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_urban_piper.ProductScreen" t-inherit="point_of_sale.ProductScreen" t-inherit-mode="extension">
<xpath expr="//button[hasclass('pay-button')]" position="attributes">
<attribute name="t-if" add="!currentOrder?.delivery_identifier" separator=" and "/>
</xpath>
<xpath expr="//div[hasclass('subpads')]/ActionpadWidget" position="attributes">
<attribute name="t-if">!currentOrder?.delivery_identifier</attribute>
</xpath>
<xpath expr="//div[hasclass('subpads')]/ActionpadWidget" position="before">
<div t-if="currentOrder?.delivery_identifier" class="d-flex">
<BackButton t-if="pos.isSmallProductScreen" onClick="() => pos.onClickBackButton()"/>
<button
t-on-click="() => this.pos.goToBack()"
class="button pay-order-button btn btn-lg w-100 highlight btn-primary"
>
Back
</button>
</div>
</xpath>
</t>
</templates>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_urban_piper.OrderReceipt" t-inherit="point_of_sale.OrderReceipt" t-inherit-mode="extension">
<xpath expr="//div[hasclass('pos-receipt')]//div[hasclass('before-footer')]" position="inside">
<div class="text-center pt-3" t-if="props.order.delivery_identifier and props.order.partner_id">
<div class="fw-bolder">Delivery Address</div>
<div t-if="props.order.partner_id.street" t-esc="props.order.partner_id.street"/>
<div t-if="props.order.partner_id.street2" t-esc="props.order.partner_id.street2"/>
<div>
<span t-if="props.order.partner_id.city"><t t-esc="props.order.partner_id.city"/> </span>
<span t-if="props.order.partner_id.state_id"><t t-esc="props.order.partner_id.state_id.name"/> </span>
<span t-if="props.order.partner_id.zip" t-esc="props.order.partner_id.zip"/>
</div>
</div>
</xpath>
</t>
</templates>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_urban_piper.ReceiptHeader" t-inherit="point_of_sale.ReceiptHeader" t-inherit-mode="extension">
<xpath expr="//div[hasclass('pos-receipt-contact')]" position="after">
<t t-if="order.delivery_provider_id?.name and order.getProviderOrderId">
<span class="fs-3">
<t t-esc="order.delivery_provider_id?.name"/>
<span>: </span><t t-esc="order.getProviderOrderId.slice(0, -4)"/>
<span class="fw-bolder"><t t-esc="order.getProviderOrderId.slice(-4)"/></span>
</span>
</t>
<t t-if="order.delivery_identifier">
<span class="fs-3">UrbanPiper: <t t-esc="order.delivery_identifier" /></span>
</t>
</xpath>
</t>
</templates>
@@ -0,0 +1,18 @@
import { useEffect } from "@odoo/owl";
import { patch } from "@web/core/utils/patch";
import { SearchBar } from "@point_of_sale/app/screens/ticket_screen/search_bar/search_bar";
patch(SearchBar.prototype, {
setup() {
super.setup();
useEffect(
() => {
this.state.searchInput =
this.props.config.defaultSearchDetails.searchTerm ||
this.state.searchInput ||
"";
},
() => [this.props.config.defaultSearchDetails]
);
},
});
@@ -0,0 +1,262 @@
import { onWillUpdateProps } from "@odoo/owl";
import { _t } from "@web/core/l10n/translation";
import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { TicketScreen } from "@point_of_sale/app/screens/ticket_screen/ticket_screen";
import { patch } from "@web/core/utils/patch";
import { SelectionPopup } from "@point_of_sale/app/components/popups/selection_popup/selection_popup";
import { orderInfoPopup } from "@pos_urban_piper/point_of_sale_overrirde/app/components/popups/order_info_popup/order_info_popup";
patch(TicketScreen, {
props: {
...TicketScreen.props,
upState: { optional: true },
},
defaultProps: {
...TicketScreen.defaultProps,
upState: "",
},
});
patch(TicketScreen.prototype, {
setup() {
super.setup(...arguments);
this.order_status = {
placed: "Placed",
acknowledged: "Acknowledged",
food_ready: "Food Ready",
dispatched: "Dispatched",
completed: "Completed",
cancelled: "Cancelled",
};
this.state.upState = this.props.upState;
onWillUpdateProps((nextProps) => {
Object.assign(this.state, nextProps.stateOverride || {});
this.state.upState = nextProps.upState;
});
},
/**
* @override
* Add two search fields.
*/
_getSearchFields() {
return Object.assign({}, super._getSearchFields(...arguments), {
DELIVERYPROVIDER: {
repr: (order) => order.getDeliveryProviderName(),
displayName: _t("Delivery Channel"),
modelField: "delivery_provider_id.name",
},
ORDERSTATUS: {
repr: (order) => order.getOrderStatus(),
displayName: _t("Delivery Order Status"),
modelField: "delivery_status",
},
});
},
async _handleResponse(response, order, new_status) {
const { is_success, message } = response;
if (!is_success) {
this.pos.notification.add(message, { type: "warning", sticky: false });
return false;
}
order.delivery_status = new_status;
return true;
},
async _updateScreenState(order, filterState, upState = "") {
this.state.upState = upState;
await this.onSearch({
fieldName: "DELIVERYPROVIDER",
searchTerm: order?.delivery_provider_id?.name,
});
await this.onFilterSelected(filterState);
},
async _updateOrderStatus(order, status, code = null) {
const urban_piper_test = JSON.parse(order.delivery_json)?.order?.urban_piper_test;
const response = await this.pos.data.call("pos.config", "order_status_update", [
this.pos.config.id,
order.id,
status,
code,
urban_piper_test,
{
orderPrepTime: order.prep_time,
},
]);
return response;
},
async _acceptOrder(order) {
const syncedOrder = this.pos.models["pos.order"].get(order.id);
const response = await this._updateOrderStatus(syncedOrder, "Acknowledged");
const status = await this._handleResponse(response, syncedOrder, "acknowledged");
if (status) {
await this.pos._sendDeliveryOrderForPreparation(syncedOrder);
this._updateScreenState(syncedOrder, "ACTIVE_ORDERS");
syncedOrder.uiState.orderAcceptTime = luxon.DateTime.now().ts;
}
},
async _rejectOrder(order) {
if (
["deliveroo", "", "hungerstation"].includes(order.delivery_provider_id.technical_name)
) {
return this.dialog.add(AlertDialog, {
title: _t("Error"),
body: _t(`Rejecting this order is not allowed for "%(providerName)s"`, {
providerName: order.delivery_provider_id.name,
}),
});
}
this.dialog.add(SelectionPopup, {
title: _t("Reject Order"),
list: [
{ id: 1, item: "item_out_of_stock", label: _t("Product is out of Stock") },
{ id: 2, item: "store_closed", label: _t("Store is Closed") },
{ id: 3, item: "store_busy", label: _t("Store is Busy") },
{ id: 4, item: "rider_not_available", label: _t("Rider is Not Available") },
{ id: 5, item: "invalid_item", label: _t("Invalid Product") },
{ id: 6, item: "out_of_delivery_radius", label: _t("Out of Delivery Radius") },
{ id: 7, item: "connectivity_issue", label: _t("Connectivity Issue") },
{ id: 8, item: "total_missmatch", label: _t("Total Missmatch") },
{ id: 9, item: "option_out_of_stock", label: _t("Variants/Addons out of Stock") },
{ id: 10, item: "invalid_option", label: _t("Invalid Variant/Addons") },
{ id: 11, item: "unspecified", label: _t("Others") },
],
getPayload: async (code) => {
const last_order_status = order.delivery_status;
order.state = "cancel";
const response = await this._updateOrderStatus(order, "Cancelled", code);
const status = await this._handleResponse(response, order, "cancelled");
if (status) {
if (
Object.keys(order.last_order_preparation_change.lines).length == 0 &&
last_order_status !== "placed"
) {
if (order.general_customer_note) {
order.last_order_preparation_change.general_customer_note =
order.general_customer_note;
}
await this.pos.checkPreparationStateAndSentOrderInPreparation(order, {
cancelled: true,
});
}
await this._updateScreenState(order, "ACTIVE_ORDERS");
await this.pos.deleteOrders([order]);
await this.pos.afterOrderDeletion();
this.setSelectedOrder(this.pos.getOrder());
}
},
});
},
async _doneOrder(order) {
const response = await this._updateOrderStatus(order, "Food Ready");
const status = await this._handleResponse(response, order, "food_ready");
if (status) {
this._updateScreenState(order, "SYNCED", "DONE");
}
await this.pos.data.loadServerOrders([["uuid", "=", order.uuid]]);
// make sure the order is identified as paid.
order = this.pos.models["pos.order"].get(order.id);
this.state.selectedOrderUuid = order.uuid;
order.setScreenData({ name: "" });
await super._doneOrder(...arguments);
},
async _dispatchOrder(order) {
const response = await this._updateOrderStatus(order, "Dispatched");
await this._handleResponse(response, order, "dispatched");
},
async _completeOrder(order) {
const response = await this._updateOrderStatus(order, "Completed");
await this._handleResponse(response, order, "completed");
},
async _onInfoOrder(order) {
this.dialog.add(orderInfoPopup, {
order: order,
order_status: this.order_status,
});
},
/**
* @override
* Return results based on upState.
*/
getFilteredOrderList() {
const orders = super.getFilteredOrderList();
if (!this.state.upState) {
return orders;
}
const statusMapping = {
NEW: "placed",
ONGOING: "acknowledged",
DONE: ["food_ready", "dispatched", "completed"],
};
const filteredOrders = orders.filter((order) => {
if (this.state.upState === "DONE") {
return statusMapping.DONE.includes(order.delivery_status);
}
return order.delivery_status === statusMapping[this.state.upState];
});
return filteredOrders;
},
fetchOrderOtp() {
return JSON.parse(this.order.delivery_json)?.order?.details?.ext_platforms?.[0].id;
},
/**
* @override
*/
getDate(order) {
if (order?.delivery_identifier) {
if (
order.date_order.toLocal().startOf("day").ts ===
luxon.DateTime.now().startOf("day").ts
) {
return _t("Today");
}
return order.date_order.toFormat("MM/dd/yyyy");
}
return super.getDate(order);
},
/**
* @override
*/
async onFilterSelected(selectedFilter) {
if (this.state.upState && this.state.filter != selectedFilter) {
this.state.upState = "";
}
super.onFilterSelected(selectedFilter);
},
/**
* @override
*/
async onSearch(search) {
if (this.state.upState && this.state.search != search) {
this.state.upState = "";
}
super.onSearch(search);
},
/**
* @override
*/
postRefund(destinationOrder) {
destinationOrder.isDeliveryRefundOrder = this.getSelectedOrder()?.delivery_identifier
? true
: false;
return super.postRefund(destinationOrder);
},
});
@@ -0,0 +1,3 @@
.ticket-screen .info-column {
width: 30px;
}
@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_urban_piper.TicketScreen" t-inherit="point_of_sale.TicketScreen" t-inherit-mode="extension">
<xpath expr="//tr//td[5]" position="after">
<td class="align-middle">
<t t-if="order.delivery_provider_id">
<t t-set="order_otp" t-value="fetchOrderOtp()"/>
<div class="d-flex">
<t t-esc="order.delivery_provider_id.name"/>
<t t-if="order_otp"> - <t t-esc="order_otp"/></t>
</div>
</t>
</td>
</xpath>
<xpath expr="//tr//td[6]" position="after">
<td class="align-middle">
<t t-if="order.delivery_status" t-esc="this.order_status[order.delivery_status]"/>
</td>
</xpath>
<xpath expr="//td[@name='delete-column']" position="before">
<td class="text-end info-column">
<t t-if="order?.delivery_identifier">
<button t-on-click="() => this._onInfoOrder(order)" class="btn btn-link btn-lg text-info">
<i class="fa fa-info-circle" aria-hidden="true"/>
</button>
</t>
</td>
</xpath>
<xpath expr="//div[hasclass('pads')]//div[hasclass('pads')]//button" position="after">
<t t-set="order" t-value="getSelectedOrder()"/>
<t t-if="order.delivery_status and order.state !== 'paid'">
<div class="container" t-if="order.delivery_status != 'cancelled' and order.delivery_status != 'completed' and order.delivery_status != 'food_ready' and order.delivery_status != 'dispatched'">
<div class="row">
<div class="col-sm container">
<div class="row">
<button class="button validation h-50 w-100 btn btn-info rounded fw-bolder py-3 col-sm" t-on-click="() => this.setOrder(order)">
<span class="fs-1 d-block">Edit</span>
</button>
<button t-if="order.delivery_status != 'completed'or order.delivery_status != 'food_ready'" class="button validation h-50 w-100 btn btn-secondary rounded fw-bolder py-3 col-sm" t-attf-class="{{ !ui.isSmall ? 'ms-2' : ''}}" t-on-click="() => this._rejectOrder(order)">
<span class="fs-1 d-block">Reject</span>
</button>
</div>
</div>
<button class="button validation h-50 w-100 btn btn-lg btn-primary rounded fw-bolder py-3" t-attf-class="{{ !ui.isSmall ? 'mt-2' : ''}}" t-if="order.delivery_status == 'acknowledged'" t-on-click="() => this._doneOrder(order)">
<span class="fs-1 d-block">Mark as ready</span>
</button>
<button t-if="order.delivery_status == 'placed'" class="button validation h-50 w-100 btn btn-lg btn-primary rounded fw-bolder py-3" t-attf-class="{{ !ui.isSmall ? 'mt-2' : ''}}" t-on-click="() => this._acceptOrder(order)">
<span class="fs-1 d-block">Accept</span>
</button>
</div>
</div>
</t>
</xpath>
<xpath expr="//div[hasclass('pads')]//div[hasclass('pads')]" position="attributes">
<attribute name="t-if">!isOrderSynced</attribute>
</xpath>
<xpath expr="//div[hasclass('pads')]//div[hasclass('pads')]//button[hasclass('load-order-button')]" position="attributes">
<attribute name="t-if">!_selectedSyncedOrder.delivery_identifier</attribute>
</xpath>
<xpath expr="//div[hasclass('switchpane')]" position="attributes">
<attribute name="t-if">!order?.delivery_identifier and ui.isSmall</attribute>
</xpath>
<xpath expr="//div[@name='delete-column']" position="before">
<div class="d-flex align-items-center info-column">
<CenteredIcon t-if="ui.isSmall and order?.delivery_identifier" icon="'fa fa-info-circle'" class="'btn btn-lg'" t-on-click="() => this._onInfoOrder(order)"/>
</div>
</xpath>
<xpath expr="//ActionpadWidget" position="after">
<button t-if="order?.deliveryOrderType == 'self' and order.delivery_status == 'food_ready'" class="btn-switchpane w-100 btn btn-primary rounded secondary fw-bold review-button mt-2" t-on-click="() => this._dispatchOrder(order)">
<span class="fs-3">Dispatch Order</span>
</button>
<button t-if="order?.deliveryOrderType == 'self' and order.delivery_status == 'dispatched'" class="btn-switchpane w-100 btn btn-primary rounded secondary fw-bold review-button mt-2" t-on-click="() => this._completeOrder(order)">
<span class="fs-3">Complete Order</span>
</button>
</xpath>
<xpath expr="//ActionpadWidget" position="attributes">
<attribute name="t-if">(this.pos.cashier._role !== 'minimal') and (order?.deliveryOrderType != 'self' or order.delivery_status == 'completed')</attribute>
</xpath>
<xpath expr="//OrderDisplay" position="attributes">
<attribute name="orderAcceptTime">_selectedSyncedOrder?.uiState?.orderAcceptTime</attribute>
<attribute name="orderPrepTime">_selectedSyncedOrder?.prep_time</attribute>
</xpath>
</t>
</templates>
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_urban_piper.OrderChangeReceipt" t-inherit="point_of_sale.OrderChangeReceipt" t-inherit-mode="extension">
<xpath expr="//div[hasclass('receipt-header')]//div[hasclass('pos-receipt-title')]" position="after">
<div class="pos-receipt-title" t-if="data.delivery_provider_id">
Delivery/Online
</div>
</xpath>
<xpath expr="//div[hasclass('receipt-header')]//div[hasclass('preset-name')]" position="attributes">
<attribute name="t-if">!data.delivery_provider_id</attribute>
</xpath>
<xpath expr="//div[hasclass('receipt-header')]//div[hasclass('o-employee-name')]" position="after">
<div t-if="data.delivery_provider_id">
<span class="fw-bolder" t-out="data.delivery_provider_id.name"/> #<t t-out="data.order_otp"/>
</div>
</xpath>
<xpath expr="//div[hasclass('pos-receipt-body')]" position="after">
<div t-if="data.delivery_provider_id and data.changes.title !== 'CANCELLED'" class="fs-2 fst-italic">
<span>Preparation Time: <t t-out="data.prep_time"/> Min</span>
</div>
</xpath>
</t>
</templates>
@@ -0,0 +1,41 @@
import { PosOrder } from "@point_of_sale/app/models/pos_order";
import { patch } from "@web/core/utils/patch";
patch(PosOrder.prototype, {
setup(vals) {
super.setup(vals);
this.isDeliveryRefundOrder = false;
},
initState() {
super.initState();
this.uiState = {
...this.uiState,
orderAcceptTime: 0,
};
},
getDeliveryProviderName() {
return this.delivery_provider_id ? this.delivery_provider_id.name : "";
},
getOrderStatus() {
return this.delivery_status ? this.delivery_status : "";
},
get isDirectSale() {
return Boolean(super.isDirectSale && !this.delivery_identifier);
},
get deliveryOrderType() {
const deliveryJson = JSON.parse(this?.delivery_json || "{}");
return deliveryJson?.order?.details?.ext_platforms?.[0]?.delivery_type;
},
isFutureOrder() {
return false;
},
get getProviderOrderId() {
return JSON.parse(this.delivery_json || "{}").order?.details?.ext_platforms?.[0].id || "";
},
});
@@ -0,0 +1,50 @@
import { Order } from "@pos_enterprise/app/components/order/order";
import { patch } from "@web/core/utils/patch";
import { useService } from "@web/core/utils/hooks";
patch(Order.prototype, {
/**
* @override
*/
setup() {
super.setup();
this.orm = useService("orm");
this.order_status = {
placed: "Placed",
acknowledged: "Acknowledged",
food_ready: "Food Ready",
dispatched: "Dispatched",
completed: "Completed",
cancelled: "Cancelled",
};
},
/**
* @override
*/
async doneOrder() {
super.doneOrder();
if (this.order.pos_order_id.delivery_identifier) {
await this.orm.call("pos.config", "order_status_update", [
this.order.pos_order_id.raw.config_id,
this.order.pos_order_id.id,
"Food Ready",
null,
this.order.urban_piper_test,
]);
}
},
_computeDuration() {
if (this.order.pos_order_id.delivery_identifier) {
const total_order_time = this.order.create_date
.setZone("local")
.plus({ minutes: this.order.pos_order_id.prep_time || 0 });
return Math.max(
Math.round((total_order_time.ts - luxon.DateTime.now().ts) / (1000 * 60)),
0
);
}
return super._computeDuration();
},
});
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_urban_piper.Order" t-inherit="pos_enterprise.Order" t-inherit-mode="extension">
<xpath expr="//div[hasclass('o_pdis_order_card_header_top')]" position="before">
<div
class="o_pdis_order_card_header_top_delivery d-flex flex-nowrap align-items-center justify-content-between px-2 pb-1 fw-bold"
t-att-class="this.cardColor"
t-if="this.order.pos_order_id.delivery_identifier">
<div class="w-100 text-truncate flex-shrink-1">
<img class="me-1" t-att-src="'/web/image?model=pos.delivery.provider&amp;field=image_128&amp;id=' + this.order.pos_order_id.delivery_provider_id.id" style="height: 28px"/>
<span class="me-1" t-esc="this.order.delivery_provider_name"/>
<t t-if="this.order.order_otp">
#<t t-esc="this.order.order_otp"/>
</t>
</div>
<div>
<t t-esc="this.order_status[this.order.pos_order_id.delivery_status]"/>
</div>
</div>
</xpath>
<xpath expr="//div[hasclass('o_pdis_order_card_header_top')]" position="attributes">
<attribute name="t-if">!this.order.pos_order_id.delivery_identifier</attribute>
</xpath>
<xpath expr="//div[hasclass('o_pdis_alert-timer')]/div[hasclass('rounded-pill')]" position="before">
<div class="rounded-pill py-1 px-3" t-if="this.order.pos_order_id.delivery_identifier"
t-att-class="{
'bg-white': this.state.duration >= 5,
'o_pdis_is_alert text-bg-danger': this.state.duration lt 5,
'o_pdis_archive-order cursor-pointer': this.prepDisplay.lastStage.id === this.order.stageId
}">
<div t-if="this.prepDisplay.lastStage.id !== this.order.stageId">
<i class="fa fa-clock-o pe-1" aria-hidden="true"/>
<span t-esc="this.state.duration" />'
</div>
</div>
</xpath>
<xpath expr="//div[hasclass('o_pdis_alert-timer')]/div[2]" position="attributes">
<attribute name="t-if">!this.order.pos_order_id.delivery_identifier</attribute>
</xpath>
</t>
</templates>
+8
View File
@@ -0,0 +1,8 @@
import { formatDateTime } from "@web/core/l10n/dates";
const { DateTime } = luxon;
/**
* This method converts time from milliseconds to the user's time zone.
*/
export function getTime(time) {
return formatDateTime(DateTime.fromMillis(time));
}