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>
113 lines
4.5 KiB
Bash
Executable File
113 lines
4.5 KiB
Bash
Executable File
#!/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
|