Tower: upload laundry_management 19.0.19.0.4 (via marketplace)

This commit is contained in:
2026-05-01 15:00:30 +00:00
parent 63d6a65f65
commit 79fe584847

View File

@@ -0,0 +1,363 @@
/** @odoo-module */
import { patch } from "@web/core/utils/patch";
import { _t } from "@web/core/l10n/translation";
import { PosStore } from "@point_of_sale/app/services/pos_store";
import { ask, makeAwaitable } from "@point_of_sale/app/utils/make_awaitable_dialog";
import { LaundryQuickCreatePartner } from "@laundry_management/js/quick_create_partner";
import { LaundryOrdersViewPopup } from "@laundry_management/js/view_laundry_orders";
import { LaundryOrderTypePopup } from "@laundry_management/js/popups/laundry_order_type_popup";
import { LaundryOrderAttributePopup } from "@laundry_management/js/popups/laundry_order_attribute_popup";
import { LaundryDeliveryDetailsPopup } from "@laundry_management/js/popups/laundry_delivery_details_popup";
/**
* Check whether a POS order contains at least one laundry-service product.
*/
function hasLaundryProduct(order) {
return order.lines.some(
(line) => line.product_id?.product_tmpl_id?.is_laundry_service
);
}
/** A line is a laundry SERVICE line — excludes settlement product. */
function isLaundryServiceLine(line) {
const tmpl = line.product_id?.product_tmpl_id;
return tmpl?.is_laundry_service && !tmpl?.is_laundry_settlement;
}
patch(PosStore.prototype, {
// -- Laundry notification after sync --
async postSyncAllOrders(orders) {
await super.postSyncAllOrders(orders);
for (const order of orders) {
if (order.laundry_order_id) {
this.notification.add(_t("Laundry Order Created"), {
type: "success",
});
}
}
},
// -- Force customer before payment screen (laundry orders only) --
async pay() {
const currentOrder = this.getOrder();
if (!currentOrder) {
return;
}
if (hasLaundryProduct(currentOrder) && !currentOrder.getPartner()) {
const confirmed = await ask(this.env.services.dialog, {
title: _t("Customer Required"),
body: _t(
"This order contains laundry items. Please select or create a customer."
),
});
if (confirmed) {
const partner = await this.selectPartner();
if (!partner) {
return;
}
} else {
return;
}
}
return super.pay();
},
// -- Quick create partner (name + phone + street, with duplicate check) --
async editPartner(partner) {
if (partner) {
return super.editPartner(partner);
}
// New partner: show lightweight popup
const result = await makeAwaitable(this.dialog, LaundryQuickCreatePartner, {});
if (!result) {
return false;
}
// Check for existing partner by phone
const existing = await this.data.call(
"res.partner",
"laundry_find_by_phone",
[result.phone]
);
if (existing) {
const useExisting = await ask(this.env.services.dialog, {
title: _t("Customer Already Exists"),
body: _t(
'A customer "%s" already exists with this phone number. Use existing customer?',
existing.name
),
});
if (useExisting) {
const partners = await this.data.read("res.partner", [existing.id]);
return partners[0] || false;
}
return false;
}
// Create new partner
const partnerId = await this.data.call(
"res.partner",
"laundry_quick_create",
[{ name: result.name, phone: result.phone, street: result.street || "" }]
);
const partners = await this.data.read("res.partner", [partnerId]);
return partners[0] || false;
},
// -- Settle laundry dues via native POS PaymentScreen --
async settleLaundryDues() {
const currentOrder = this.getOrder();
const partner = currentOrder?.getPartner();
if (!partner) {
this.notification.add(_t("Please select a customer first."), {
type: "warning",
});
const selectedPartner = await this.selectPartner();
if (!selectedPartner) {
return;
}
return this.settleLaundryDues();
}
// Fetch outstanding dues
const dues = await this.data.call(
"res.partner",
"get_laundry_dues",
[partner.id]
);
if (!dues || dues.total_due <= 0) {
this.notification.add(
_t('"%s" has no outstanding laundry dues.', partner.name),
{ type: "info" }
);
return;
}
// Find the settlement product (is_laundry_settlement = true)
const settlementProduct = this.models["product.product"]
.getAll()
.find((p) => p.product_tmpl_id?.is_laundry_settlement);
if (!settlementProduct) {
this.notification.add(
_t("Settlement product not configured. Please check laundry settings."),
{ type: "danger" }
);
return;
}
// Create a dedicated settlement order
const order = this.addNewOrder();
order.setPartner(partner);
// Add settlement product with total_due as price
await this.addLineToCurrentOrder(
{
product_id: settlementProduct,
product_tmpl_id: settlementProduct.product_tmpl_id,
price_unit: dues.total_due,
qty: 1,
},
{},
false
);
// Navigate to native POS PaymentScreen
this.navigate("PaymentScreen", {
orderUuid: order.uuid,
});
},
// ─────────────────────────────────────────────────────────────────
// Laundry order type popup chain
// ─────────────────────────────────────────────────────────────────
async addLineToCurrentOrder(vals, opts, configure) {
const result = await super.addLineToCurrentOrder(vals, opts, configure);
try {
await this.maybeAskLaundryOrderType(this.getOrder());
} catch (e) {
console.error("Laundry order-type prompt failed:", e);
}
return result;
},
/**
* Trigger the type/attributes/delivery popup chain when the FIRST
* laundry-service line is added to an order — and only when:
* - feature is enabled in pos.config
* - order type not already chosen
* - the order has at least one laundry-service line (excludes settlement)
* - ask-on-first-line is enabled
*/
async maybeAskLaundryOrderType(order) {
if (!order || !order.lines.length) {
return;
}
const config = this.config;
if (!config?.enable_laundry_order_type) {
return;
}
if (order.laundry_order_type_id) {
return;
}
if (config.ask_laundry_order_type_on_first_line === false) {
return;
}
const laundryLines = order.lines.filter(isLaundryServiceLine);
if (!laundryLines.length) {
return;
}
// Only trigger when this is the FIRST laundry line.
if (laundryLines.length > 1) {
return;
}
await this.runLaundryOrderTypeFlow(order);
},
/**
* The popup chain itself — also reusable from the inline edit button
* in the order summary.
*/
async runLaundryOrderTypeFlow(order) {
if (!order) {
return;
}
const config = this.config;
// Step 1 — Main type
const types = (this.models["laundry.order.type"]?.getAll() || [])
.slice()
.sort((a, b) => (a.sequence || 0) - (b.sequence || 0));
if (!types.length) {
this.notification.add(
_t("No laundry order types configured. Ask a manager to add some."),
{ type: "warning" }
);
if (config.require_laundry_order_type) {
return;
}
}
const partner = order.getPartner();
const partnerDefaultType = partner?.default_laundry_order_type_id;
const defaultTypeId =
order.laundry_order_type_id?.id ||
(partnerDefaultType?.id ?? partnerDefaultType) ||
(config.default_laundry_order_type_id?.id ??
config.default_laundry_order_type_id) ||
false;
const chosenType = await makeAwaitable(this.dialog, LaundryOrderTypePopup, {
types: types,
defaultTypeId: defaultTypeId,
showIcons: !!config.show_order_type_icons,
allowSkip: !config.require_laundry_order_type,
title: _t("Select Order Type"),
});
if (chosenType === undefined) {
return; // user closed without confirming
}
order.laundry_order_type_id = chosenType
? this.models["laundry.order.type"].get(chosenType.id)
: false;
// Step 2 — Attributes (skip if disabled or nothing to show)
let chosenAttributes = [];
if (config.enable_laundry_attributes) {
const allAttrs = (this.models["laundry.order.attribute"]?.getAll() || [])
.slice()
.sort((a, b) => (a.sequence || 0) - (b.sequence || 0));
if (allAttrs.length) {
const suggestedIds = chosenType?.attribute_ids
? chosenType.attribute_ids
.map((a) => (typeof a === "object" ? a.id : a))
: [];
const partnerDefaultIds = (partner?.default_laundry_attribute_ids || [])
.map((a) => (typeof a === "object" ? a.id : a));
const preselected = [
...new Set([...suggestedIds, ...partnerDefaultIds]),
];
const result = await makeAwaitable(
this.dialog,
LaundryOrderAttributePopup,
{
attributes: allAttrs,
preselectedIds: preselected,
title: _t("Select Attributes (optional)"),
}
);
if (result !== undefined) {
chosenAttributes = result;
}
}
}
order.laundry_order_attribute_ids = chosenAttributes.map((a) =>
this.models["laundry.order.attribute"].get(a.id)
);
// Step 3 — Delivery details (only when needed)
const typeIsDelivery = !!chosenType?.is_delivery;
const attrIsDelivery = chosenAttributes.some((a) => a.is_delivery_related);
const isDelivery = typeIsDelivery || attrIsDelivery;
order.laundry_is_delivery = isDelivery;
if (isDelivery && config.require_delivery_details_if_needed) {
const requireAddress =
!!chosenType?.requires_address ||
attrIsDelivery; // delivery attribute implies need for address
const requireTime = !!chosenType?.requires_scheduled_time;
const result = await makeAwaitable(
this.dialog,
LaundryDeliveryDetailsPopup,
{
defaultAddress: order.laundry_delivery_address || "",
defaultScheduledAt: order.laundry_delivery_scheduled_at || "",
requireAddress: requireAddress,
requireScheduledTime: requireTime,
}
);
if (result) {
order.laundry_delivery_address = result.address || false;
order.laundry_delivery_scheduled_at = result.scheduledAt || false;
}
} else if (!isDelivery) {
order.laundry_delivery_address = false;
order.laundry_delivery_scheduled_at = false;
}
},
/** Inline edit handler from the order-summary header. */
async editLaundryOrderType() {
const order = this.getOrder();
if (!order) {
return;
}
if (!this.config?.allow_change_laundry_order_type_before_payment) {
this.notification.add(_t("Changing order type is disabled."), {
type: "warning",
});
return;
}
await this.runLaundryOrderTypeFlow(order);
},
// -- View laundry orders for current customer --
async viewLaundryOrders() {
const currentOrder = this.getOrder();
let partner = currentOrder?.getPartner();
if (!partner) {
partner = await this.selectPartner();
if (!partner) {
return;
}
}
const orders = await this.data.call(
"res.partner",
"get_laundry_orders_for_pos",
[partner.id, 30]
);
this.dialog.add(LaundryOrdersViewPopup, {
partnerName: partner.name,
orders: orders || [],
});
},
});