Files
erp/.claude/skills/dolibarr-tva-deductible/scripts/deductible-by-month.sh
Gabriel Radureau 585b7beb03 add dolibarr-thirdparty-completeness and dolibarr-tva-deductible
V4 bundle — two more sibling skills, both read-only, both depending
on the dolibarr base skill.

dolibarr-thirdparty-completeness:
- audit-thirdparty.sh <socid>: country-aware completeness audit for
  any thirdparty (FR: SIREN + SIRET + tva_intra; EU non-FR: tva_intra;
  extra-EU: national tax id). Generalizes the V1 KM-hardcoded script.
- audit-all-thirdparties.sh: loops over /thirdparties and surfaces a
  compact table of gaps. --clients-only / --suppliers-only flags.
- Live baseline finds 5/10 thirdparties with mandatory gaps:
  KissMetrics (US tax id), Wise Europe SA (BE tva_intra), Medialex
  (FR SIRET + tva_intra), Qonto (SIRET), Infogreffe (SIRET).

dolibarr-tva-deductible:
- deductible-by-month.sh: TVA déductible aggregated per period × rate.
- deductible-line-detail.sh: per supplier-invoice line with country-
  based CA3 bucket assignment (ligne 20 for 20 % FR, ligne 19 for
  reduced rates, ligne 17+24 for intra-UE autoliquidation).
- Live baseline: 223.22 € total TVA déductible across 13 lines.
  Wise Europe SA correctly identified as intra-UE autoliquidation;
  La Poste correctly identified as FR exempt (timbres).
- Mirrors dolibarr-tva-reconciliation on the supplier side. Together
  they give the two numbers a CA3 monthly declaration needs.

Also extends dolibarr/SKILL.md endpoint catalogue with /supplierinvoices
(noting the 403 on the /lines sub-endpoint — inline lines on the detail
endpoint make this a non-issue). dolibarr/README.md gains two new
permission checkboxes for Factures fournisseurs.

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

113 lines
4.5 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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