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:
98
.claude/skills/dolibarr-tva-reconciliation/scripts/tva-line-detail.sh
Executable file
98
.claude/skills/dolibarr-tva-reconciliation/scripts/tva-line-detail.sh
Executable file
@@ -0,0 +1,98 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user