Files
erp/.claude/skills/dolibarr-recurring-templates/scripts/list-templates.sh
Gabriel Radureau f19b1d2ef2 add dolibarr-tva-reconciliation, dolibarr-recurring-templates, dolibarr-data-snapshot
V3 bundle — three sibling skills under .claude/skills/, all read-only,
all depending on the dolibarr base skill.

dolibarr-tva-reconciliation:
- tva-by-month.sh: HT + TVA grouped by (year-month × tva_tx), ready
  for CA3 / CA12 transcription.
- tva-line-detail.sh: per-line audit trail with country-based bucket
  assignment (A1 domestic / A4 intra-UE autoliquidation / E2 export
  hors UE). Documents the French TVA mental model.
- Today every Arcodange line is E2 (KissMetrics, US, autoliquidation
  259-1° CGI). The skill scales for the day a French B2B is invoiced.

dolibarr-recurring-templates:
- list-templates.sh: probes /invoices/templates/{id} since there's no
  list endpoint. Stops after 5 consecutive empty responses.
- inspect-template.sh: full audit per template, with health checks.
- Surfaces that the "Kiss Metrics Invoice" template has frequency=0
  and nb_gen_done=0 — it is NOT auto-firing. Every KM invoice today
  was manually duplicated. Cohort-review implication: the deferred
  9-month cycle depends on Gabriel clicking "Generate" each month,
  not on a Dolibarr cron.

dolibarr-data-snapshot:
- snapshot.sh: bundles every read endpoint the dolibarr-* family uses
  into one JSON with a content_hash (sha256 of data only, excluding
  timestamp — so identical state hashes identically across runs).
- Use cases: cohort evidence packs, drift detection, archival before
  a known-risky UI change.
- V1 baseline summary captured at examples/snapshot-summary.txt
  (the ~246 KB snapshot file itself is intentionally not committed).

Also extends dolibarr/SKILL.md endpoint catalogue with
/invoices/templates/{id} (and its no-list-endpoint quirk + the
id-null sentinel for missing ids), plus links to the three new
sibling skills.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 00:01:06 +02:00

79 lines
2.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# Enumerate Arcodange recurring invoice templates.
#
# The Dolibarr API doesn't expose a list endpoint for templates. We probe
# ids 1..MAX and stop after N consecutive empty hits. An "empty" template
# has `id == null` (Dolibarr returns 200 with a hollow object for unknown ids).
#
# Usage:
# list-templates.sh [--max-id N] # default 50
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOL_CURL="${SCRIPT_DIR}/../../dolibarr/scripts/dol-curl.sh"
MAX_ID=50
EMPTY_TOLERANCE=5 # stop after this many consecutive empties
while [[ $# -gt 0 ]]; do
case "$1" in
--max-id) MAX_ID="$2"; shift 2 ;;
-h|--help) sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "list-templates.sh: unknown arg: $1" >&2; exit 2 ;;
esac
done
WORK="$(mktemp -d -t dtmpl.XXXXXX)"
trap 'rm -rf "${WORK}"' EXIT
CONSECUTIVE_EMPTY=0
FOUND=()
for id in $(seq 1 "${MAX_ID}"); do
if ! "${DOL_CURL}" "/invoices/templates/${id}" > "${WORK}/${id}.json" 2>/dev/null; then
# Network / auth error — bail rather than mis-classify
echo "list-templates.sh: error fetching id=${id}, stopping probe" >&2
break
fi
REAL=$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); print('1' if d.get('id') else '0')" "${WORK}/${id}.json")
if [[ "${REAL}" == "1" ]]; then
FOUND+=("${id}")
CONSECUTIVE_EMPTY=0
else
CONSECUTIVE_EMPTY=$((CONSECUTIVE_EMPTY+1))
rm "${WORK}/${id}.json"
if [[ ${CONSECUTIVE_EMPTY} -ge ${EMPTY_TOLERANCE} ]]; then
break
fi
fi
done
echo "# Probed ids 1..${id}, found ${#FOUND[@]} template(s) (stopped after ${EMPTY_TOLERANCE} consecutive empties)"
echo
if [[ ${#FOUND[@]} -eq 0 ]]; then
echo "# (no recurring templates found)"
exit 0
fi
python3 - "${WORK}" "${FOUND[@]}" <<'PY'
import json, sys, os, datetime
work = sys.argv[1]
ids = sys.argv[2:]
print(f"{'id':>3} {'ref':<28} {'socid':>5} {'freq':<10} {'gen':>6} {'next':<12} {'last':<12} {'sus':>3} {'auto_val':>8} {'total_ht':>10}")
print("-" * 115)
for iid in ids:
d = json.load(open(os.path.join(work, f"{iid}.json")))
freq_n = int(d.get("frequency") or 0)
freq_u = d.get("unit_frequency") or ""
freq_str = f"every {freq_n}{freq_u}" if freq_n else "OFF (0)"
nb_done = d.get("nb_gen_done") or "0"
nb_max = d.get("nb_gen_max") or "0"
gen = f"{nb_done}/{nb_max}" if nb_max != "0" else f"{nb_done}/∞"
nextd = d.get("date_when") or "-"
lastd = d.get("date_last_gen") or "-"
sus = d.get("suspended") or "0"
autoval = d.get("auto_validate") or "0"
print(f"{iid:>3} {(d.get('ref') or d.get('title') or '-'):<28} {str(d.get('socid') or '-'):>5} {freq_str:<10} {gen:>6} {nextd[:10]:<12} {lastd[:10]:<12} {sus:>3} {autoval:>8} {d.get('total_ht','-'):>10}")
PY