""" 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.")