Files
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

99 lines
3.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# Per-line TVA breakdown with the country classification needed to choose
# the right CA3 line (E2 export hors UE vs A4 autoliquidation intra-UE vs
# domestic A1).
#
# Usage:
# tva-line-detail.sh [--year YYYY] [--since YYYY-MM-DD] [--until YYYY-MM-DD]
#
# Pulls each invoice's lines + the thirdparty's country_code to assign each
# line to a CA3 bucket. Useful as the audit trail behind tva-by-month.sh.
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,12p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "tva-line-detail.sh: unknown arg: $1" >&2; exit 2 ;;
esac
done
WORK="$(mktemp -d -t tvaline.XXXXXX)"
trap 'rm -rf "${WORK}"' EXIT
"${DOL_CURL}" '/invoices?limit=500&sortfield=t.datef&sortorder=ASC' > "${WORK}/inv.json"
# Per-invoice detail + per-thirdparty country
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}/inv" "${WORK}/tp"
SOCIDS=$(python3 -c "import json,sys; print(' '.join(sorted({str(r.get('socid')) for r in json.load(open(sys.argv[1])) if r.get('socid')})))" "${WORK}/inv.json")
for id in ${IDS}; do "${DOL_CURL}" "/invoices/${id}" > "${WORK}/inv/${id}.json"; done
for s in ${SOCIDS}; do "${DOL_CURL}" "/thirdparties/${s}" > "${WORK}/tp/${s}.json"; done
python3 - "${WORK}" "${SINCE}" "${UNTIL}" "${YEAR}" <<'PY'
import json, sys, os, datetime
work, since, until, year_arg = sys.argv[1:5]
# EU member states (excl. FR — FR lines are "domestic")
EU = set("AT BE BG HR CY CZ DK EE FI DE GR HU IE IT LV LT LU MT NL PL PT RO SK SI ES SE".split())
tp = {}
for fn in os.listdir(os.path.join(work,"tp")):
try: d = json.load(open(os.path.join(work,"tp",fn)))
except json.JSONDecodeError: continue
tp[fn[:-len(".json")]] = d.get("country_code") or ""
def bucket(country, tx):
if tx and tx > 0: return f"A1 (domestic {tx}%)"
if country == "FR": return "A1 (domestic, 0%? check)"
if country in EU: return "A4 (autoliquidation intra-UE)"
if country: return "E2 (export hors UE)"
return "(unknown country)"
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
print(f"{'date':<10} {'invoice':<24} {'client':<15} {'cnty':<4} {'tx':>5} {'HT':>10} {'TVA':>8} CA3 bucket")
print("-" * 110)
all_lines = []
for fn in sorted(os.listdir(os.path.join(work,"inv"))):
try: inv = json.load(open(os.path.join(work,"inv",fn)))
except json.JSONDecodeError: continue
ts = int(inv.get("date") or 0)
if not in_window(ts): continue
dt = datetime.date.fromtimestamp(ts)
sid = str(inv.get("socid") or "")
cnty = tp.get(sid, "")
client = "tp-"+sid
for line in inv.get("lines") or []:
tx = round(float(line.get("tva_tx") or 0), 4)
ht = float(line.get("total_ht") or 0)
tva = float(line.get("total_tva") or 0)
buck = bucket(cnty, tx)
all_lines.append((dt, inv["ref"], client, cnty, tx, ht, tva, buck))
print(f"{dt} {inv['ref']:<24} {client:<15} {cnty:<4} {tx:>5.2f} {ht:>10.2f} {tva:>8.2f} {buck}")
print("-" * 110)
# Summary per bucket
import collections
buckets = collections.defaultdict(lambda: {"ht":0.0,"tva":0.0,"n":0})
for r in all_lines:
b = r[7]
buckets[b]["ht"] += r[5]; buckets[b]["tva"] += r[6]; buckets[b]["n"] += 1
print()
print("# Aggregated by CA3 bucket:")
for b, s in sorted(buckets.items()):
print(f" {b:<35} count={s['n']:>3} HT={s['ht']:>10.2f} TVA={s['tva']:>8.2f}")
PY