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>
This commit is contained in:
2026-05-29 00:01:06 +02:00
parent d34cba3fa0
commit f19b1d2ef2
16 changed files with 1554 additions and 1 deletions

View File

@@ -0,0 +1,106 @@
#!/usr/bin/env bash
# Monthly TVA basis aggregation for Arcodange — ready to transcribe onto
# CA3 (régime réel normal) or CA12 (réel simplifié) declarations.
#
# Usage:
# tva-by-month.sh [--year YYYY] [--since YYYY-MM-DD] [--until YYYY-MM-DD]
#
# Groups every visible invoice line by (year-month, tva_tx). Sums HT and TVA.
# Surfaces credit notes correctly (negative HT cancels out at the basis level).
#
# Mapping to French TVA forms (régime réel normal CA3):
# - tva_tx == 0 AND client country != FR
# → ligne A4 "Autres opérations imposables" if reverse-charge intra-UE
# → ligne E2 "Exportations hors UE" if extra-UE
# (the line script tva-line-detail.sh distinguishes these)
# - tva_tx == 20 → A1 base + 8 TVA collectée
# - tva_tx == 10 → A1 (taux réduit) base + 9 TVA collectée
# - tva_tx == 5.5 → A1 base + 9B TVA collectée
# - tva_tx == 2.1 → A1 base + 9C TVA collectée
#
# Today Arcodange is monorange autoliquidation 259-1° on KM (extra-UE), so
# everything lands on E2. As soon as a French B2B client is invoiced, the
# 20 % bucket will populate.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOL_CURL="${SCRIPT_DIR}/../../dolibarr/scripts/dol-curl.sh"
SINCE=""
UNTIL=""
YEAR=""
while [[ $# -gt 0 ]]; do
case "$1" in
--since) SINCE="$2"; shift 2 ;;
--until) UNTIL="$2"; shift 2 ;;
--year) YEAR="$2"; SINCE="$2-01-01"; UNTIL="$2-12-31"; shift 2 ;;
-h|--help) sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "tva-by-month.sh: unknown arg: $1" >&2; exit 2 ;;
esac
done
WORK="$(mktemp -d -t tvamonth.XXXXXX)"
trap 'rm -rf "${WORK}"' EXIT
"${DOL_CURL}" '/invoices?limit=500&sortfield=t.datef&sortorder=ASC' > "${WORK}/inv.json"
# Per-invoice detail fetch (the list endpoint doesn't include line-level tva_tx)
IDS=$(python3 -c "import json,sys; print(' '.join(str(r['id']) for r in json.load(open(sys.argv[1]))))" "${WORK}/inv.json")
mkdir -p "${WORK}/detail"
for id in ${IDS}; do
"${DOL_CURL}" "/invoices/${id}" > "${WORK}/detail/${id}.json"
done
python3 - "${WORK}" "${SINCE}" "${UNTIL}" "${YEAR}" <<'PY'
import json, sys, os, collections, datetime
work, since, until, year_arg = sys.argv[1:5]
def in_window(ts):
if not ts: return False
d = datetime.date.fromtimestamp(int(ts))
if since and d < datetime.date.fromisoformat(since): return False
if until and d > datetime.date.fromisoformat(until): return False
return True
# key: (yyyy-mm, tva_tx) -> {ht, tva, count}
agg = collections.defaultdict(lambda: {"ht":0.0,"tva":0.0,"count":0})
for fn in sorted(os.listdir(os.path.join(work,"detail"))):
try: inv = json.load(open(os.path.join(work,"detail",fn)))
except json.JSONDecodeError: continue
ts = int(inv.get("date") or 0)
if not in_window(ts): continue
ym = datetime.date.fromtimestamp(ts).strftime("%Y-%m")
for line in inv.get("lines") or []:
tx = float(line.get("tva_tx") or 0)
# Normalize tva_tx — Dolibarr returns "0.0000" / "20.0000" strings as numbers
tx = round(tx, 4)
ht = float(line.get("total_ht") or 0)
tva = float(line.get("total_tva") or 0)
agg[(ym, tx)]["ht"] += ht
agg[(ym, tx)]["tva"] += tva
agg[(ym, tx)]["count"] += 1
scope = f"window={since or '-inf'} → {until or '+inf'}"
if year_arg: scope = f"year {year_arg}"
print(f"# TVA basis by month × rate — {scope}")
print()
print(f"{'month':<8} {'tva_tx':>7} {'count':>5} {'basis HT':>12} {'TVA':>10} CA3 line")
print("-" * 70)
total_ht = total_tva = 0.0
for key in sorted(agg):
ym, tx = key
s = agg[key]
line = "(see tva-line-detail for cnty)" if tx == 0 else ("A1+8" if tx==20 else f"A1+? @ {tx}%")
print(f"{ym:<8} {tx:>7.4f} {s['count']:>5} {s['ht']:>12.2f} {s['tva']:>10.2f} {line}")
total_ht += s["ht"]
total_tva += s["tva"]
print("-" * 70)
print(f"{'TOTAL':>16} {' ':>13} {total_ht:>12.2f} {total_tva:>10.2f}")
print()
print("# Notes:")
print("# - Lines with tva_tx==0 require country lookup to choose CA3 E2 (export hors UE)")
print("# vs A4 (autoliquidation intra-UE). Run tva-line-detail.sh for that breakdown.")
print("# - Credit notes (AVOIRs) show as negative HT and are correctly netted at basis.")
PY