diff --git a/addons/laundry_management/static/src/js/pos_order_patch.js b/addons/laundry_management/static/src/js/pos_order_patch.js new file mode 100644 index 0000000..a61a5bc --- /dev/null +++ b/addons/laundry_management/static/src/js/pos_order_patch.js @@ -0,0 +1,133 @@ +/** @odoo-module + * + * PosOrder patch — laundry context fields. + * + * Two responsibilities: + * 1. setup(): seed primitive defaults on the reactive proxy so that + * OWL components (panel, receipt) which read these slots track + * future writes and re-render. Without seeding, assigning a fresh + * property after first render does NOT trigger reactivity. + * No fields are pulled from the loader — pure in-memory defaults. + * + * 2. serializeForORM(): inject the laundry slots into the sync_from_ui + * payload so the backend pos.order columns are populated, exactly + * like `general_customer_note`. + * - laundry_order_type_id → integer id (Many2one) + * - laundry_order_attribute_ids → [[6, 0, ids]] (Many2many "set") + * - laundry_is_delivery → boolean + * - laundry_delivery_address → string | false + * - laundry_delivery_scheduled_at → string | false + * + * NOTE: this patch does NOT touch order.lines, pricing, taxes, or any + * relational field exposed by _load_pos_data_fields. + */ +import { patch } from "@web/core/utils/patch"; +import { PosOrder } from "@point_of_sale/app/models/pos_order"; + +function pickId(v) { + if (!v) return false; + if (typeof v === "number") return v; + if (typeof v === "object" && Number.isInteger(v.id)) return v.id; + return false; +} + +function pickIds(arr) { + if (!Array.isArray(arr)) return []; + const out = []; + const seen = new Set(); + for (const v of arr) { + const id = pickId(v); + if (id && !seen.has(id)) { + seen.add(id); + out.push(id); + } + } + return out; +} + +function normalizeDateTime(value) { + if (!value || typeof value !== "string") return false; + + let v = value; + + // Remove timezone like +03:00 or -03:00 + const tzMatch = v.match(/([+-]\d{2}:\d{2})$/); + if (tzMatch) { + v = v.replace(tzMatch[0], ""); + } + + // Remove Z + if (v.endsWith("Z")) { + v = v.slice(0, -1); + } + + // Remove milliseconds + if (v.includes(".")) { + v = v.split(".")[0]; + } + + // Replace T with space + v = v.replace("T", " "); + + // Ensure seconds exist + if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(v)) { + v += ":00"; + } + + return v; +} + +patch(PosOrder.prototype, { + setup(...args) { + super.setup(...args); + // Seed defaults on the reactive proxy. Assigning after first + // render only triggers OWL reactivity if the key already exists + // on the proxy at observation time. + if (this.laundry_order_type_id === undefined) { + this.laundry_order_type_id = false; + } + if (this.laundry_order_attribute_ids === undefined) { + this.laundry_order_attribute_ids = []; + } + if (this.laundry_is_delivery === undefined) { + this.laundry_is_delivery = false; + } + if (this.laundry_delivery_address === undefined) { + this.laundry_delivery_address = false; + } + if (this.laundry_delivery_scheduled_at === undefined) { + this.laundry_delivery_scheduled_at = false; + } + }, + + initState(...args) { + super.initState(...args); + // Reactive flag for POS Mode system: when true, the order is a + // dedicated settlement context — products cannot be added, + // quantities cannot be edited, line cannot be deleted. + // Seeded here so OWL reactivity tracks future writes. + if (this.uiState && !("is_laundry_settle_due" in this.uiState)) { + this.uiState.is_laundry_settle_due = false; + } + }, + + serializeForORM(opts = {}) { + const data = super.serializeForORM(opts); + + const typeId = pickId(this.laundry_order_type_id); + data.laundry_order_type_id = typeId || false; + + const attrIds = pickIds(this.laundry_order_attribute_ids); + data.laundry_order_attribute_ids = [[6, 0, attrIds]]; + + data.laundry_is_delivery = !!this.laundry_is_delivery; + data.laundry_delivery_address = this.laundry_delivery_address || false; + // ABSOLUTE GUARANTEE: nothing with a "T", ".000", or timezone reaches + // the backend. Run the full normalizer even if upstream already did; + // normalize(normalize(x)) === normalize(x), so this is a zero-cost net. + data.laundry_delivery_scheduled_at = + normalizeDateTime(this.laundry_delivery_scheduled_at) || false; + + return data; + }, +});