#!/usr/bin/env bash # Timeline of all payments against KissMetrics invoices, sorted by date. # # Usage: # km-payment-timeline.sh [--year YYYY] [--since YYYY-MM-DD] [--until YYYY-MM-DD] # # Shows, for each KM payment: date, invoice ref, amount, type, bank account, # and a running cumulative balance. Useful for the deferred-cycle tracking # (compare actual cash receipts against the contractual schedule). set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DOL_CURL="${SCRIPT_DIR}/../../dolibarr/scripts/dol-curl.sh" KM_SOCID=1 SINCE="" UNTIL="" while [[ $# -gt 0 ]]; do case "$1" in --since) SINCE="$2"; shift 2 ;; --until) UNTIL="$2"; shift 2 ;; --year) SINCE="$2-01-01"; UNTIL="$2-12-31"; shift 2 ;; -h|--help) sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; *) echo "km-payment-timeline.sh: unknown arg: $1" >&2; exit 2 ;; esac done WORK="$(mktemp -d -t kmtimeline.XXXXXX)" trap 'rm -rf "${WORK}"' EXIT "${DOL_CURL}" '/invoices?limit=100&sortfield=t.datef&sortorder=DESC' > "${WORK}/inv.json" "${DOL_CURL}" '/bankaccounts' > "${WORK}/banks.json" # Pull per-invoice payments only for KM invoices KM_IDS=$(python3 -c " import json, sys d = json.load(open(sys.argv[1])) print(' '.join(str(r['id']) for r in d if str(r.get('socid'))==sys.argv[2])) " "${WORK}/inv.json" "${KM_SOCID}") mkdir -p "${WORK}/pay" for id in ${KM_IDS}; do "${DOL_CURL}" "/invoices/${id}/payments" > "${WORK}/pay/${id}.json" done python3 - "${WORK}" "${SINCE}" "${UNTIL}" "${KM_SOCID}" <<'PY' import json, sys, os, datetime work, since, until, km_socid = sys.argv[1:5] invoices = {str(r["id"]): r for r in json.load(open(os.path.join(work,"inv.json")))} banks = {str(b["id"]): b for b in json.load(open(os.path.join(work,"banks.json")))} def parse_iso(s): return datetime.datetime.strptime(s, "%Y-%m-%d %H:%M:%S") def in_window(dt): if since and dt.date() < datetime.date.fromisoformat(since): return False if until and dt.date() > datetime.date.fromisoformat(until): return False return True rows = [] for fn in os.listdir(os.path.join(work,"pay")): iid = fn[:-len(".json")] inv = invoices.get(iid) if not inv or 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: dt = parse_iso(p["date"]) if not in_window(dt): continue rows.append((dt, inv["ref"], float(p.get("amount") or 0), p.get("type",""), p.get("ref",""), p.get("fk_bank_line",""))) rows.sort(key=lambda r: r[0]) # We don't have a direct fk_bank_line → fk_account mapping from /bankaccounts # alone (would need /bankaccounts/{id}/lines). Show fk_bank_line as-is and # annotate with the IBAN-bearing account when only one account holds receipts. # Receivable accounts in this instance: QON1, WIS2, CCA1. print(f"{'date':<10} {'invoice':<22} {'amount':>10} {'type':<6} {'ref':<20} {'bank_line':<10} running") print("-" * 110) running = 0.0 for dt, ref, amt, typ, pref, fbl in rows: running += amt print(f"{dt.date()} {ref:<22} {amt:>10.2f} {typ:<6} {pref:<20} bl={fbl:<8} {running:>10.2f}") print("-" * 110) print(f"# {len(rows)} payment(s), net cash receipts: {running:.2f}") print() print("# Bank accounts known to this Dolibarr (label / IBAN-leading):") for bid, b in sorted(banks.items(), key=lambda kv: int(kv[0])): iban = b.get("iban") or "-" label = b.get("label") or b.get("ref") or "-" print(f"# id={bid} ref={b.get('ref','-'):<6} label={label:<32} country={b.get('country_code','-'):<3} iban={iban[:18]+'...' if len(iban)>18 else iban}") PY