From 969414e649817a98798f052284725e917df246bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phan=20Sainl=C3=A9ger?= Date: Thu, 28 May 2026 10:47:19 +0200 Subject: [PATCH] [IMP] detect missing pos.order hashes and regenerate them --- lib/python/regenerate_pos_hashes.py | 141 ++++++++++++++++++++++++++++ scripts/finalize_db.sh | 23 +++++ 2 files changed, 164 insertions(+) create mode 100644 lib/python/regenerate_pos_hashes.py diff --git a/lib/python/regenerate_pos_hashes.py b/lib/python/regenerate_pos_hashes.py new file mode 100644 index 0000000..61a8bbe --- /dev/null +++ b/lib/python/regenerate_pos_hashes.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +"""Regenerate all POS order inalterability hashes and sequence numbers. + +This script should only be called when the SQL pre-check in finalize_db.sh +has confirmed that some pos_orders are missing l10n_fr_hash or +l10n_fr_secure_sequence_number. + +It resets ALL sequence numbers and hashes from scratch, in chronological order +(date_order ASC, id ASC), and re-establishes the full hash chain. + +Usage (via Odoo shell): + odoo-shell -d < regenerate_pos_hashes.py +""" +import logging +from hashlib import sha256 + +_logger = logging.getLogger(__name__) + + +def regenerate_all(): + PosOrder = env['pos.order'] + Company = env['res.company'] + + for company in Company.search([]): + if not company._is_accounting_unalterable(): + continue + + print(f"\n{'='*60}") + print(f" Company: {company.name}") + print(f"{'='*60}") + + orders = PosOrder.search([ + ('state', 'in', ['paid', 'done', 'invoiced']), + ('company_id', '=', company.id), + ], order='date_order ASC, id ASC') + + if not orders: + print(" No orders to process.") + continue + + n = len(orders) + + # ── Step 1: Reset all fields ───────────────────────────── + print("\n--- Resetting fields ---") + env.cr.execute(""" + UPDATE pos_order + SET l10n_fr_hash = NULL, + l10n_fr_secure_sequence_number = NULL, + previous_order_id = NULL + WHERE id IN %s + """, (tuple(orders.ids),)) + env.invalidate_all() + env.cr.commit() + print(f" ✓ {n} orders reset") + + # ── Step 2: Assign sequence numbers ────────────────────── + print("\n--- Assigning sequence numbers ---") + for i, order in enumerate(orders, start=1): + env.cr.execute(""" + UPDATE pos_order + SET l10n_fr_secure_sequence_number = %s + WHERE id = %s + """, (i, order.id)) + env.invalidate_all() + env.cr.commit() + print(f" ✓ Sequence numbers 1 → {n} assigned") + + # ── Step 3: Compute previous_order_id ──────────────────── + print("\n--- Computing previous_order_id ---") + orders = PosOrder.search([ + ('state', 'in', ['paid', 'done', 'invoiced']), + ('company_id', '=', company.id), + ], order='l10n_fr_secure_sequence_number ASC') + orders._compute_previous_order() + env.cr.commit() + print(" ✓ OK") + + # ── Step 4: Compute hashes (with cache invalidation after each write) ─ + print("\n--- Computing hashes ---") + success = errors = 0 + + for idx in range(n): + order = PosOrder.search([ + ('company_id', '=', company.id), + ('l10n_fr_secure_sequence_number', '=', idx + 1), + ], limit=1) + + if not order: + continue + + try: + order._compute_string_to_hash() + + prev = order.previous_order_id + prev_hash = prev.l10n_fr_hash if prev else '' + if not prev_hash: + prev_hash = '' + + computed_hash = sha256( + (prev_hash + order.l10n_fr_string_to_hash).encode('utf-8') + ).hexdigest() + + env.cr.execute( + "UPDATE pos_order SET l10n_fr_hash = %s WHERE id = %s", + (computed_hash, order.id) + ) + + env.invalidate_all() + print(f" ✓ {order.name} (seq {idx+1})") + success += 1 + + except Exception as e: + print(f" ✗ {order.name} (seq {idx+1}) : {e}") + errors += 1 + import traceback + traceback.print_exc() + + env.cr.commit() + + remaining = PosOrder.search_count([ + ('state', 'in', ['paid', 'done', 'invoiced']), + ('company_id', '=', company.id), + '|', ('l10n_fr_hash', '=', False), + ('l10n_fr_hash', '=', None), + ]) + print(f"\n Final result: {success} hashes written, {errors} errors, " + f"{remaining} remaining") + + # Reset sequence for future orders + seq = company.l10n_fr_pos_cert_sequence_id + if seq: + env.cr.execute( + "UPDATE ir_sequence SET number_next = %s WHERE id = %s", + (n + 1, seq.id) + ) + print(f" ✓ ir_sequence number_next set to {n + 1}") + + print("\n✓ Done.") + + +regenerate_all() diff --git a/scripts/finalize_db.sh b/scripts/finalize_db.sh index 31b5638..3a97ac9 100755 --- a/scripts/finalize_db.sh +++ b/scripts/finalize_db.sh @@ -44,6 +44,29 @@ PYTHON_SCRIPT="${SCRIPT_DIR}/lib/python/cleanup_modules.py" echo "Uninstall obsolete add-ons with script $PYTHON_SCRIPT ..." exec_python_script_in_odoo_shell "$DB_NAME" "$DB_NAME" "$PYTHON_SCRIPT" +# ──────────────────────────────────────────────────────────── +# Regenerate POS inalterability hashes if needed +# ──────────────────────────────────────────────────────────── +HASHES_NEEDED=$(query_postgres_container " + SELECT COUNT(*) + FROM pos_order po + JOIN res_company rc ON rc.id = po.company_id + WHERE po.state IN ('paid', 'done', 'invoiced') + AND rc.l10n_fr_pos_cert_sequence_id IS NOT NULL + AND (po.l10n_fr_hash IS NULL OR po.l10n_fr_secure_sequence_number IS NULL) +" "$DB_NAME") + +if [[ "$HASHES_NEEDED" =~ ^[0-9]+$ && "$HASHES_NEEDED" -gt 0 ]]; then + echo "" + echo "Found $HASHES_NEEDED pos.order(s) with missing inalterability hash or sequence number." + echo "Regenerating all POS hashes..." + PYTHON_SCRIPT="${SCRIPT_DIR}/lib/python/regenerate_pos_hashes.py" + exec_python_script_in_odoo_shell "$DB_NAME" "$DB_NAME" "$PYTHON_SCRIPT" + echo "POS hash regeneration completed." +else + echo "No missing POS hashes detected." +fi + # Give back the right to user to access to the tables # docker exec -u 70 "$DB_CONTAINER_NAME" pgm chown "$FINALE_SERVICE_NAME" "$DB_NAME"