5 patchs MySQLisme désormais cuits dans l'image erpnext-custom via Dockerfile (COPY+RUN apply_pg_patches.py). Débloque submit stock/compta sur PostgreSQL (vérifié: Stock Reconciliation submit OK sur conteneur frais). frappe_pg aussi proprement bake + ajouté à apps.txt (shim chargé eager pour toutes requêtes, token inclus). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
106 lines
4.6 KiB
Python
106 lines
4.6 KiB
Python
"""
|
|
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)
|
|
4. repost_item_valuation.py — get_max_period_closing_date : qb Max + ORDER BY creation → get_value(order_by=None)
|
|
5. accounts/general_ledger.py — validate_against_pcv : get_value agrégat MAX + ORDER BY creation
|
|
→ order_by=None. DÉBLOQUE le submit des transactions stock/compta (GroupingError PG).
|
|
"""
|
|
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,
|
|
)
|
|
|
|
# 4) repost_item_valuation — get_max_period_closing_date : qb Max(...) + ORDER BY creation
|
|
# (GroupingError PG) → get_value(..., order_by=None)
|
|
patch_file(
|
|
f"{BENCH}/erpnext/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py",
|
|
lambda s: re.sub(
|
|
r'table = frappe\.qb\.DocType\("Period Closing Voucher"\).*?return query\[0\]\[0\] if query and query\[0\]\[0\] else None',
|
|
'return frappe.db.get_value("Period Closing Voucher", {"company": company, "docstatus": 1}, "max(period_end_date)", order_by=None)',
|
|
s, count=1, flags=re.DOTALL),
|
|
lambda s: 'max(period_end_date)", order_by=None' in s,
|
|
)
|
|
|
|
# 5) general_ledger — validate_against_pcv : get_value avec agrégat MAX + ORDER BY creation par
|
|
# défaut (GroupingError PG) → ajout order_by=None. C'est CE patch qui débloque le submit stock/compta.
|
|
patch_file(
|
|
f"{BENCH}/erpnext/erpnext/accounts/general_ledger.py",
|
|
lambda s: s.replace(
|
|
'{"docstatus": 1, "company": company}, [{"MAX": "period_end_date"}]',
|
|
'{"docstatus": 1, "company": company}, [{"MAX": "period_end_date"}], order_by=None', 1),
|
|
lambda s: '[{"MAX": "period_end_date"}], order_by=None' in s,
|
|
)
|
|
|
|
print("DONE — redémarrer le conteneur pour recharger les workers.")
|