#!/usr/bin/env bash # Inspect one recurring invoice template end-to-end. # # Usage: # inspect-template.sh # # Prints the template's identification, schedule (frequency / unit / # next-fire / last-fire / counts), customer, payment terms, lines, and # a HEALTH section that calls out common issues: # - frequency == 0 → template NOT actually firing (manual generation only) # - nb_gen_done == 0 but children invoices exist → manual duplications # - date_when in the past → overdue / paused # - suspended == 1 → explicit pause set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DOL_CURL="${SCRIPT_DIR}/../../dolibarr/scripts/dol-curl.sh" if [[ $# -lt 1 ]]; then echo "inspect-template.sh: missing id. Usage: inspect-template.sh " >&2 exit 2 fi ID="$1" WORK="$(mktemp -d -t inspect.XXXXXX)" trap 'rm -rf "${WORK}"' EXIT "${DOL_CURL}" "/invoices/templates/${ID}" > "${WORK}/template.json" "${DOL_CURL}" '/invoices?limit=500&sortfield=t.datef&sortorder=ASC' > "${WORK}/inv.json" python3 - "${WORK}" "${ID}" <<'PY' import json, sys, os, datetime, html work, tid = sys.argv[1], sys.argv[2] t = json.load(open(os.path.join(work, "template.json"))) if not t.get("id"): print(f"inspect-template.sh: no template at id={tid}", file=sys.stderr) sys.exit(1) socid = t.get("socid") or "" # Fetch the customer name if we can tp_name = "-" if socid: import subprocess try: out = subprocess.check_output([ os.path.join(os.path.dirname(__file__) if False else os.environ.get("DOL_CURL","./dol-curl.sh"), ), f"/thirdparties/{socid}" ], stderr=subprocess.DEVNULL) tp_name = json.loads(out).get("name") or "-" except Exception: pass freq_n = int(t.get("frequency") or 0) freq_u = t.get("unit_frequency") or "" nb_done = int(t.get("nb_gen_done") or 0) nb_max = int(t.get("nb_gen_max") or 0) date_when = t.get("date_when") or "" date_last = t.get("date_last_gen") or "" suspended = str(t.get("suspended") or "0") == "1" auto_val = str(t.get("auto_validate") or "0") == "1" gen_pdf = str(t.get("generate_pdf") or "0") == "1" print("=" * 80) print(f" Template {tid} — {t.get('ref') or t.get('title') or '(unnamed)'}") print("=" * 80) print(f" Customer : socid={socid}") print(f" Schedule : frequency={freq_n} {freq_u} {'(OFF — manual generation only)' if freq_n == 0 else ''}") print(f" Counts : generated={nb_done} / max={nb_max or 'unbounded'}") print(f" Next fire date : {date_when or '(unset)'}") print(f" Last fire date : {date_last or '(none)'}") print(f" Suspended : {suspended}") print(f" Auto-validate : {auto_val}") print(f" Generate PDF : {gen_pdf}") print(f" Payment terms : mode={t.get('mode_reglement_code') or '-'} cond={t.get('cond_reglement_code') or '-'} ({t.get('cond_reglement_doc') or '-'})") print(f" Totals : HT={t.get('total_ht','-')} TVA={t.get('total_tva','-')} TTC={t.get('total_ttc','-')}") print(f" Bank account : fk_account={t.get('fk_account') or '-'}") print() print(" Lines:") for L in t.get("lines") or []: desc = html.unescape(L.get("desc") or "") print(f" - ref={L.get('product_ref','-')} qty={L.get('qty','-')} subprice={L.get('subprice','-')} tva={L.get('tva_tx','-')} HT={L.get('total_ht','-')}") if desc: for ln in desc.splitlines(): if ln.strip(): print(f" {ln.strip()}") # Children: invoices that quote this template in note_private invoices = json.load(open(os.path.join(work, "inv.json"))) title_ref = (t.get("ref") or t.get("title") or "").strip() children = [] for inv in invoices: np = (inv.get("note_private") or "").lower() if title_ref and title_ref.lower() in np: children.append(inv) print() print(f" Generated children (by note_private match on '{title_ref}'):") if not children: print(" (none found — surprising if nb_gen_done > 0 or if you expected this to fire)") else: for inv in sorted(children, key=lambda x: int(x.get("date") or 0)): ts = int(inv.get("date") or 0) dt = datetime.date.fromtimestamp(ts).strftime("%Y-%m-%d") if ts else "-" print(f" - {dt} id={inv['id']:>4} {inv['ref']:<24} HT={inv.get('total_ht','-'):>10} paye={inv.get('paye','-')}") # Health print() print(" Health checks:") issues = [] if freq_n == 0: issues.append("frequency=0 — template is NOT auto-generating; every child was created manually") if nb_done == 0 and children: issues.append(f"nb_gen_done=0 but {len(children)} child invoice(s) match by note — they were duplicated, not auto-generated") if date_when and date_when != "": try: when = datetime.date.fromisoformat(date_when[:10]) if when < datetime.date.today(): issues.append(f"date_when ({date_when[:10]}) is in the past — next fire is overdue or paused") except ValueError: pass if suspended: issues.append("suspended=1 — template explicitly paused") if not issues: print(" [OK] Template looks consistent.") for i in issues: print(f" [!!] {i}") sys.exit(0 if not issues else 1) PY