#!/usr/bin/env bash # Monthly TVA déductible aggregation for Arcodange — the supplier-side # counterpart to dolibarr-tva-reconciliation. # # Usage: # deductible-by-month.sh [--year YYYY] [--since YYYY-MM-DD] [--until YYYY-MM-DD] # # Groups every visible supplier invoice line by (year-month, tva_tx) and sums # HT and TVA. The TVA figure feeds the CA3 lignes 19 / 20 (déductible). # # Mapping to French CA3: # - tva_tx == 20 → ligne 20 (TVA déductible normale 20 %) # - tva_tx == 10 → ligne 19 (taux intermédiaire) # - tva_tx == 5.5 → ligne 19 (taux réduit) # - tva_tx == 2.1 → ligne 19 (taux particulier) # - tva_tx == 0 → likely autoliquidation intra-UE (achats à l'étranger) # → goes on ligne 17 (auto-collected) AND ligne 24 (déductible) # run deductible-line-detail.sh for the per-supplier breakdown 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,20p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; *) echo "deductible-by-month.sh: unknown arg: $1" >&2; exit 2 ;; esac done WORK="$(mktemp -d -t deduc.XXXXXX)" trap 'rm -rf "${WORK}"' EXIT "${DOL_CURL}" '/supplierinvoices?limit=500' > "${WORK}/inv.json" IDS=$(python3 -c "import json,sys; print(' '.join(str(r['id']) for r in json.load(open(sys.argv[1])) if r.get('id')))" "${WORK}/inv.json") mkdir -p "${WORK}/detail" for id in ${IDS}; do "${DOL_CURL}" "/supplierinvoices/${id}" > "${WORK}/detail/${id}.json"; done python3 - "${WORK}" "${SINCE}" "${UNTIL}" "${YEAR}" <<'PY' import json, sys, os, collections, datetime work, since, until, year_arg = sys.argv[1:5] 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 agg = collections.defaultdict(lambda: {"ht":0.0,"tva":0.0,"count":0}) for fn in sorted(os.listdir(os.path.join(work,"detail"))): try: inv = json.load(open(os.path.join(work,"detail",fn))) except json.JSONDecodeError: continue ts = int(inv.get("date") or 0) if not in_window(ts): continue ym = datetime.date.fromtimestamp(ts).strftime("%Y-%m") lines = inv.get("lines") or [] if lines: for line in lines: 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) agg[(ym, tx)]["ht"] += ht agg[(ym, tx)]["tva"] += tva agg[(ym, tx)]["count"] += 1 else: # Fallback: aggregate at invoice level (no lines available) # Derive an effective tva_tx if possible: TVA / HT × 100 ht = float(inv.get("total_ht") or 0) tva = float(inv.get("total_tva") or 0) tx = round((tva / ht * 100), 4) if ht else 0.0 agg[(ym, tx)]["ht"] += ht agg[(ym, tx)]["tva"] += tva agg[(ym, tx)]["count"] += 1 def ca3_line(tx): if tx == 20: return "ligne 20" if tx == 10: return "ligne 19 (10 %)" if tx == 5.5: return "ligne 19 (5,5 %)" if tx == 2.1: return "ligne 19 (2,1 %)" if tx == 0: return "ligne 17 + 24 (autoliquidation) — verify with line-detail" return f"manual @ {tx}%" scope = f"window={since or '-inf'} → {until or '+inf'}" if year_arg: scope = f"year {year_arg}" print(f"# TVA déductible by month × rate — {scope}") print() print(f"{'month':<8} {'tva_tx':>7} {'count':>5} {'basis HT':>12} {'TVA ded':>10} CA3 line") print("-" * 78) total_ht = total_tva = 0.0 for key in sorted(agg): ym, tx = key s = agg[key] print(f"{ym:<8} {tx:>7.4f} {s['count']:>5} {s['ht']:>12.2f} {s['tva']:>10.2f} {ca3_line(tx)}") total_ht += s["ht"] total_tva += s["tva"] print("-" * 78) print(f"{'TOTAL':>16} {' ':>13} {total_ht:>12.2f} {total_tva:>10.2f}") print() print("# Notes:") print("# - This is TVA déductible (supplier side). For TVA collectée (customer side),") print("# use dolibarr-tva-reconciliation/scripts/tva-by-month.sh.") print("# - tva_tx==0 lines may be either truly exempt (e.g. La Poste timbres)") print("# or autoliquidation intra-UE (e.g. Wise). Run deductible-line-detail.sh.") PY