From 5ee5d4f5cdd90d047a3f0c537232bcbc54d947f4 Mon Sep 17 00:00:00 2001 From: git_admin Date: Fri, 1 May 2026 15:00:53 +0000 Subject: [PATCH] Tower: upload laundry_management 19.0.19.0.4 (via marketplace) --- .../tools/laundry_pos_link_healthcheck.py | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 addons/laundry_management/tools/laundry_pos_link_healthcheck.py diff --git a/addons/laundry_management/tools/laundry_pos_link_healthcheck.py b/addons/laundry_management/tools/laundry_pos_link_healthcheck.py new file mode 100644 index 0000000..5e20160 --- /dev/null +++ b/addons/laundry_management/tools/laundry_pos_link_healthcheck.py @@ -0,0 +1,158 @@ +"""POS ↔ laundry.order link healthcheck. + +Run via: + docker exec -i odoo-laundry-system-odoo19-1 odoo shell \\ + -c /etc/odoo/odoo.conf -d dev --no-http \\ + < extra-addons/laundry_management/tools/laundry_pos_link_healthcheck.py + +Read-only by default. To heal missing links + repair the tracking-code +sequence, set HEAL=True at the top. + +What it does +──────────── +1. Reports the laundry tracking-code sequence vs MAX(tracking_code) — + this is the most common cause of the silent "no laundry.order + created" symptom (UniqueViolation swallowed by the savepoint). +2. Lists the latest 10 POS orders with linkage status + line scan. +3. Lists the latest 10 laundry.order rows. +4. Counts unlinked POS orders that SHOULD have a laundry.order + (have ≥1 laundry-service line, no settlement-only). +5. Verifies that the laundry-side hand-off is callable as a Cashier- + group user (permission smoke). +6. Optional heal: backfills missing links + repairs the sequence. +""" +import traceback + +env = self.env # noqa: F821 — provided by odoo shell + +# Toggle to True to backfill missing links + repair the sequence. +HEAL = False + +print("=" * 70) +print("POS ↔ LAUNDRY LINK HEALTHCHECK HEAL =", HEAL) +print("=" * 70) + +# ── 1. Sequence sanity check ────────────────────────────────────────── +SEQ_CODE = "laundry.order.line.tracking" +seq = env["ir.sequence"].sudo().search([("code", "=", SEQ_CODE)], limit=1) +if seq: + env.cr.execute( + """SELECT COALESCE(MAX( + CAST(NULLIF(REGEXP_REPLACE(tracking_code,'[^0-9]','','g'),'') + AS INTEGER) + ), 0) FROM laundry_order_line WHERE tracking_code IS NOT NULL""" + ) + max_existing = env.cr.fetchone()[0] or 0 + drift = seq.number_next - (max_existing + 1) + status = "OK" if drift >= 0 else f"BEHIND by {-drift} (collisions imminent)" + print(f"\n[1] sequence {SEQ_CODE}") + print(f" number_next = {seq.number_next} " + f"max_existing = {max_existing} drift = {drift} {status}") +else: + print(f"\n[1] !!! sequence {SEQ_CODE} not found") + +# ── 2. Latest 10 POS orders with linkage ───────────────────────────── +print("\n[2] last 10 pos.order with linkage") +env.cr.execute(""" + SELECT id, name, pos_reference, partner_id, amount_total, state, + create_date, laundry_order_id + FROM pos_order + ORDER BY id DESC + LIMIT 10 +""") +for row in env.cr.dictfetchall(): + link = row["laundry_order_id"] or "-" + print(f" pos.id={row['id']:>3} name={(row['name'] or '')[:24]:<24} " + f"ref={(row['pos_reference'] or '-'):<22} " + f"link={link:>4} state={row['state']}") + +# ── 3. Latest 10 laundry.order ──────────────────────────────────────── +print("\n[3] last 10 laundry.order") +env.cr.execute(""" + SELECT id, name, pos_order_id, partner_id, amount_total, amount_due, state + FROM laundry_order + ORDER BY id DESC + LIMIT 10 +""") +for row in env.cr.dictfetchall(): + print(f" lo.id={row['id']:>3} name={row['name']:<22} " + f"pos={row['pos_order_id'] or '-':>4} " + f"total={row['amount_total']:>7} due={row['amount_due']:>5} " + f"state={row['state']}") + +# ── 4. Unlinked POS orders that SHOULD have a laundry.order ────────── +print("\n[4] unlinked POS orders that have laundry-service lines") +unlinked = env["pos.order"].search( + [("laundry_order_id", "=", False)], order="id desc", +) +should_have = [] +no_laundry_lines = [] +for o in unlinked: + has_laundry = any( + line.product_id + and line.product_id.product_tmpl_id.is_laundry_service + and not line.product_id.product_tmpl_id.is_laundry_settlement + for line in o.lines + ) + if has_laundry: + should_have.append(o) + else: + no_laundry_lines.append(o) +print(f" should-have-link (skipped): {len(should_have)}") +print(f" no-laundry-lines (correct skip): {len(no_laundry_lines)}") +for o in should_have[:10]: + print(f" pos.id={o.id:>3} name={o.name!r} partner={o.partner_id.name!r}") + +# ── 5. Permission smoke — Cashier can run the hand-off ─────────────── +print("\n[5] permission smoke — call _maybe_create_laundry_order as Cashier") +cashier_group = env.ref( + "laundry_management.group_laundry_cashier", raise_if_not_found=False, +) +# Odoo 19: res.groups.users → res.groups.user_ids +group_users = ( + getattr(cashier_group, "user_ids", None) + or getattr(cashier_group, "users", None) +) if cashier_group else None +if group_users: + cashier = group_users[0] + print(f" using existing cashier: {cashier.login}") + if should_have: + target = should_have[0] + try: + with env.cr.savepoint(): + target.with_user(cashier).sudo()._maybe_create_laundry_order() + print(f" [OK] handoff callable as Cashier on POS {target.id}") + except Exception: + print(f" !!! permission failure:") + traceback.print_exc() +else: + print(" no cashier user available — skipping (admin-only env)") + +# ── 6. Optional heal ───────────────────────────────────────────────── +if HEAL: + print("\n[6] HEALING") + if seq: + env["laundry.order.line"].sudo()._repair_tracking_sequence() + seq_after = env["ir.sequence"].sudo().search( + [("code", "=", SEQ_CODE)], limit=1, + ) + print(f" sequence repaired → number_next = {seq_after.number_next}") + healed = 0 + for o in should_have: + try: + with env.cr.savepoint(): + o.sudo()._maybe_create_laundry_order() + if o.laundry_order_id: + healed += 1 + except Exception: + print(f" heal failed on POS {o.id}:") + traceback.print_exc() + print(f" healed {healed} of {len(should_have)} missing links") + env.cr.commit() + print(" committed.") +else: + print("\n[6] heal SKIPPED — set HEAL=True at top of script to backfill") + +print("\n" + "=" * 70) +print("HEALTHCHECK DONE") +print("=" * 70)