#!/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