From 79fe584847432ebebae010e8a98cd0f6ead3582c Mon Sep 17 00:00:00 2001 From: git_admin Date: Fri, 1 May 2026 15:00:30 +0000 Subject: [PATCH] Tower: upload laundry_management 19.0.19.0.4 (via marketplace) --- .../static/src/js/pos_store_patch.js.bak | 363 ++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 addons/laundry_management/static/src/js/pos_store_patch.js.bak diff --git a/addons/laundry_management/static/src/js/pos_store_patch.js.bak b/addons/laundry_management/static/src/js/pos_store_patch.js.bak new file mode 100644 index 0000000..ca81390 --- /dev/null +++ b/addons/laundry_management/static/src/js/pos_store_patch.js.bak @@ -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 || [], + }); + }, +});