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>
This commit is contained in:
80
.claude/skills/dolibarr-payments-state/scripts/payments-by-month.sh
Executable file
80
.claude/skills/dolibarr-payments-state/scripts/payments-by-month.sh
Executable file
@@ -0,0 +1,80 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user