From 536bd2dfa8ba07e53568e4f7ff4e4cf59358bb24 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Thu, 4 Jun 2026 06:16:09 -0400 Subject: [PATCH] PG patches reproductibles (idempotent): ORDER BY NULL (product.py) + CurDate (item.py heatmap) + CURRENT_DATE() filet (frappe_pg) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Volatils dans le conteneur → à ré-appliquer après rebuild. Period Closing Voucher GROUP BY = TODO (#53). Co-Authored-By: Claude Opus 4.8 (1M context) --- frappe-setup/apply_pg_patches.py | 85 ++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 frappe-setup/apply_pg_patches.py diff --git a/frappe-setup/apply_pg_patches.py b/frappe-setup/apply_pg_patches.py new file mode 100644 index 0000000..ff3beb6 --- /dev/null +++ b/frappe-setup/apply_pg_patches.py @@ -0,0 +1,85 @@ +""" +Patchs ERPNext ⇆ PostgreSQL (MySQLismes) — idempotent. +====================================================== +ERPNext v16 contient des requêtes SQL propres à MySQL qui cassent sur PostgreSQL. +Le shim `frappe_pg` en traduit certaines, mais (a) il s'applique paresseusement +(hook on_session_creation → absent en auth par token) et (b) il a des trous. +On corrige donc À LA SOURCE, de façon idempotente. + +⚠️ Ces patchs modifient le code des apps DANS le conteneur → VOLATILES (perdus au +rebuild d'image / recreate). À ré-exécuter après chaque rebuild, idéalement dans +CHAQUE conteneur applicatif (backend, queue-*, scheduler), puis redémarrer. +TODO persistance: cuire dans l'image (overlay) ou via un entrypoint. + +Exécution (par conteneur applicatif) : + docker cp apply_pg_patches.py erpnext-backend-1:/tmp/ + docker exec erpnext-backend-1 sh -lc \ + "cd /home/frappe/frappe-bench && env/bin/python /tmp/apply_pg_patches.py" + docker restart erpnext-backend-1 # recharger les workers gunicorn + +Patchs inclus : + 1. utilities/product.py — `ORDER BY NULL` retiré (validation de variante) + 2. stock/.../item.py — get_timeline_data : CurDate() → add_to_date (heatmap fiche Item) + 3. frappe_pg query_transformers — CURRENT_DATE()/CURDATE() → CURRENT_DATE (filet général) + +À FAIRE (non encore résolu) : + - Period Closing Voucher : requête GROUP BY/ORDER BY creation invalide en PG + (GroupingError) qui bloque le submit des transactions de stock/compta. À localiser. +""" +import re + +BENCH = "/home/frappe/frappe-bench/apps" + + +def patch_file(path, transform, marker): + try: + s = open(path).read() + except FileNotFoundError: + print("ABSENT", path) + return + if marker(s): + print("déjà patché :", path.split("/apps/")[-1]) + return + ns = transform(s) + if ns == s: + print("ANCRE INTROUVABLE :", path.split("/apps/")[-1]) + return + open(path, "w").write(ns) + print("PATCHED :", path.split("/apps/")[-1]) + + +# 1) product.py — ORDER BY NULL (MySQL) invalide en PG +patch_file( + f"{BENCH}/erpnext/erpnext/utilities/product.py", + lambda s: re.sub(r"ORDER BY\s+NULL", "", s), + lambda s: not re.search(r"ORDER BY\s+NULL", s), +) + +# 2) item.py — get_timeline_data : CurDate() - Interval → date calculée en Python +patch_file( + f"{BENCH}/erpnext/erpnext/stock/doctype/item/item.py", + lambda s: s.replace( + ".where(sle.posting_date > CurDate() - Interval(years=1))", + ".where(sle.posting_date > frappe.utils.add_to_date(frappe.utils.nowdate(), years=-1))", + 1, + ), + lambda s: "add_to_date(frappe.utils.nowdate(), years=-1)" in s, +) + +# 3) frappe_pg — filet général CURRENT_DATE()/CURDATE() → CURRENT_DATE +QT = f"{BENCH}/frappe_pg/frappe_pg/postgres/query_transformers.py" +MARK = "# PG-FIX CURRENT_DATE()" +ADD = ( + " " + MARK + "\n" + " import re as _re\n" + " query = _re.sub(r'\\bCURRENT_DATE\\s*\\(\\s*\\)', 'CURRENT_DATE', query, flags=_re.IGNORECASE)\n" + " query = _re.sub(r'\\bCURDATE\\s*\\(\\s*\\)', 'CURRENT_DATE', query, flags=_re.IGNORECASE)\n" +) +patch_file( + QT, + lambda s: s.replace(" query = convert_date_format(query)\n", + " query = convert_date_format(query)\n" + ADD, 1), + lambda s: MARK in s, +) + +print("DONE — redémarrer le conteneur pour recharger les workers.")