Files
erp/.claude/skills/dolibarr-payments-state/scripts/payments-by-month.sh
Gabriel Radureau e7abfd5e22 add dolibarr-payments-state skill for cash receipt tracking
V2 in the dolibarr-* family. Three workflows:

- km-payment-state.sh: per-invoice reconciliation (TTC vs sum of
  payments) with OK / PARTIAL / UNPAID / OVERPAID classification.
  More honest than the `paye` boolean for deferred-cycle agreements.
- km-payment-timeline.sh: all KM payments sorted by date with
  cumulative balance — the foundation for cohort-review deferred
  9-month-cycle tracking (actual cash receipts vs contractual schedule).
- payments-by-month.sh: monthly aggregation, KM-scoped by default
  or --all-clients for accounting basis.

Also updates dolibarr/SKILL.md endpoint catalogue with
/invoices/{id}/payments (note the date-as-string vs unix-epoch quirk)
and /bankaccounts, plus captures the corresponding examples.

V1 baseline of live data: KM is fully reconciled across 5 invoices
(1 avoir + 4 regular), 8160 € total cash receipts spread Feb/Mar/Apr 2026,
all on WISE EURO (BE).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 18:52:48 +02:00

81 lines
2.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# Monthly cash-receipt aggregation for KissMetrics payments.
#
# Usage:
# payments-by-month.sh [--year YYYY] [--all-clients]
#
# By default only sums KissMetrics payments (socid=1). With --all-clients,
# iterates every visible invoice. Useful as the foundation for the
# cohort-review deferred-cycle dashboard (sum-by-month + count-by-month).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOL_CURL="${SCRIPT_DIR}/../../dolibarr/scripts/dol-curl.sh"
KM_SOCID=1
ALL=0
YEAR=""
while [[ $# -gt 0 ]]; do
case "$1" in
--year) YEAR="$2"; shift 2 ;;
--all-clients) ALL=1; shift ;;
-h|--help) sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "payments-by-month.sh: unknown arg: $1" >&2; exit 2 ;;
esac
done
WORK="$(mktemp -d -t pmonth.XXXXXX)"
trap 'rm -rf "${WORK}"' EXIT
"${DOL_CURL}" '/invoices?limit=100&sortfield=t.datef&sortorder=DESC' > "${WORK}/inv.json"
# Filter invoice ids
if [[ "${ALL}" == "1" ]]; then
IDS=$(python3 -c "import json,sys; print(' '.join(str(r['id']) for r in json.load(open(sys.argv[1]))))" "${WORK}/inv.json")
else
IDS=$(python3 -c "import json,sys; print(' '.join(str(r['id']) for r in json.load(open(sys.argv[1])) if str(r.get('socid'))==sys.argv[2]))" "${WORK}/inv.json" "${KM_SOCID}")
fi
mkdir -p "${WORK}/pay"
for id in ${IDS}; do
"${DOL_CURL}" "/invoices/${id}/payments" > "${WORK}/pay/${id}.json"
done
python3 - "${WORK}" "${YEAR}" "${ALL}" "${KM_SOCID}" <<'PY'
import json, sys, os, collections, datetime
work, year, all_clients, km_socid = sys.argv[1], sys.argv[2], sys.argv[3]=="1", sys.argv[4]
invoices = {str(r["id"]): r for r in json.load(open(os.path.join(work,"inv.json")))}
monthly = collections.defaultdict(lambda: {"count":0, "amount":0.0})
for fn in sorted(os.listdir(os.path.join(work,"pay"))):
iid = fn[:-len(".json")]
inv = invoices.get(iid)
if not inv: continue
if not all_clients and str(inv.get("socid")) != km_socid: continue
try: pays = json.load(open(os.path.join(work,"pay",fn)))
except json.JSONDecodeError: continue
for p in pays:
d = datetime.datetime.strptime(p["date"], "%Y-%m-%d %H:%M:%S").date()
if year and d.strftime("%Y") != year: continue
key = d.strftime("%Y-%m")
monthly[key]["count"] += 1
monthly[key]["amount"] += float(p.get("amount") or 0)
scope = "all clients" if all_clients else f"KissMetrics (socid={km_socid})"
print(f"# Monthly cash receipts — scope: {scope}" + (f" — year {year}" if year else ""))
print()
print(f"{'month':<8} {'count':>5} {'amount':>12} cumulative")
print("-" * 50)
cum = 0.0
for key in sorted(monthly):
s = monthly[key]
cum += s["amount"]
print(f"{key:<8} {s['count']:>5} {s['amount']:>12.2f} {cum:>10.2f}")
print("-" * 50)
total_count = sum(v["count"] for v in monthly.values())
total_amount = sum(v["amount"] for v in monthly.values())
print(f"{'TOTAL':<8} {total_count:>5} {total_amount:>12.2f}")
PY