V8 — first inbound-side skill. Closes the loop from "bill arrives by email"
to "ready to enter in Dolibarr UI". Read-only at every layer.
What ships:
- arcodange-email-ingest/scripts/zoho-curl.sh OAuth wrapper with token cache
(50 min TTL, mode 600) — avoids
hitting Zoho OAuth rate limit on
every invocation.
- arcodange-email-ingest/scripts/email-list.sh List candidates in /Inbox/books
(where the books@ alias auto-
routes mail). --candidates-only
filter on supplier patterns or
attachments. --all-folders to
scan everything.
- arcodange-email-ingest/scripts/email-inspect.sh Pull message + attachments,
pdftotext on each PDF, heuristic
extract (supplier, ref, dates,
totals, VAT rate), emit Dolibarr
supplier-invoice draft JSON.
Architecture choice — Zoho API (not IMAP):
- books@arcodange.fr is an alias of gabrielradureau@arcodange.fr → one OAuth
refresh_token covers everything.
- Gmail folded in via forwarding (arcodange@gmail.com → books@) — no Google
API setup, no app-passwords, no second OAuth flow.
- Token-based auth, no SCA rabbit hole.
V8.0 baseline (in /Inbox/books):
- 3 candidates: Mistral AI facture, Anthropic Stripe receipt (Fwd Gmail),
INPI payment receipt (Fwd Gmail).
- Heuristic extraction is best-effort: works on amounts/refs for some
templates, misses others (Mistral PDF format, Stripe receipt layout).
- --save-pdf <DIR> lets the operator grab the PDFs for manual entry when
the heuristic falls short.
Rate-limit pitfall documented: Zoho OAuth refresh has an aggressive throttle
("too many requests continuously"). The cache file at $TMPDIR/zoho-access-$USER
(mode 600, 50 min TTL) prevents this; on 401 the wrapper auto-refreshes once
and retries.
V8.1+ ideas in SKILL.md out-of-scope:
- mark ingested emails (IMAP flag or Zoho label)
- body text extraction (inline-HTML invoices)
- per-template parsers or LLM-based extraction
- IMAP fallback for non-Zoho mailboxes
CLI: bin/arcodange email {list|inspect|curl} integrated.
Base updates: dolibarr/SKILL.md cross-link, dolibarr/README.md env schema
extended with ZOHO_CLIENT_ID/SECRET/REFRESH_TOKEN/DC.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
297 lines
12 KiB
Bash
Executable File
297 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# arcodange — read-only operational CLI for the Arcodange Dolibarr ERP.
|
||
#
|
||
# Usage: arcodange <command> [subcommand] [args]
|
||
#
|
||
# Run `arcodange help` for the full command list.
|
||
#
|
||
# This is a thin dispatcher: every subcommand delegates to a script under
|
||
# .claude/skills/<skill>/scripts/. The skills (markdown SKILL.md files)
|
||
# remain the source of behaviour documentation; this CLI is the human-
|
||
# friendly entry point so you don't have to spell out 5-component paths.
|
||
|
||
set -euo pipefail
|
||
|
||
# --- Locate the project root ----------------------------------------------
|
||
# Strategy:
|
||
# 1. git rev-parse --show-toplevel (works when CWD is inside the repo).
|
||
# 2. Walk up from this script's directory to find a sibling .claude/skills.
|
||
# Either approach handles being run from a worktree without surprises.
|
||
|
||
if SROOT=$(git rev-parse --show-toplevel 2>/dev/null) && [[ -d "${SROOT}/.claude/skills" ]]; then
|
||
:
|
||
else
|
||
SROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
if [[ ! -d "${SROOT}/.claude/skills" ]]; then
|
||
echo "arcodange: cannot find project root (no .claude/skills/ found)" >&2
|
||
exit 2
|
||
fi
|
||
fi
|
||
|
||
SKILLS="${SROOT}/.claude/skills"
|
||
DOLC="${SKILLS}/dolibarr/scripts/dol-curl.sh"
|
||
|
||
# --- Help text -----------------------------------------------------------
|
||
|
||
usage() {
|
||
cat <<'EOF'
|
||
arcodange — read-only Arcodange Dolibarr CLI.
|
||
|
||
USAGE
|
||
arcodange <command> [subcommand] [args...]
|
||
|
||
COMMANDS
|
||
|
||
tva French TVA monthly preparation
|
||
collect [--year|--since|--until] TVA collectée by month × rate (CA3 A1/A4/E2)
|
||
collect-detail [--year|--since|--until] Per-line audit, customer side
|
||
deductible [--year|--since|--until] TVA déductible by month × rate (CA3 19/20/17+24)
|
||
deductible-detail [--year|--since|--until] Per-line audit, supplier side
|
||
summary [--year|--since|--until] Composite CA3-ready monthly summary
|
||
|
||
invoice Customer invoices (KissMetrics)
|
||
list [--since YYYY-MM-DD] Table of KM invoices with payment state
|
||
audit <invoice-id> JSON facts + PDF mandatory-mention audit
|
||
|
||
thirdparty Clients + suppliers completeness
|
||
audit <socid> Country-aware audit for one thirdparty
|
||
audit-all [--clients-only|--suppliers-only] Audit every visible thirdparty
|
||
|
||
payments Cash receipts (KissMetrics-side)
|
||
state [--since YYYY-MM-DD] Per-invoice TTC vs payments reconciliation
|
||
timeline [--year|--since|--until] Payment timeline with cumulative balance
|
||
by-month [--year|--all-clients] Monthly aggregation
|
||
|
||
templates Recurring invoice templates
|
||
list [--max-id N] Enumerate templates (probes ids)
|
||
inspect <template-id> Full template audit with health checks
|
||
|
||
snapshot [--out FILE|--print-only] Bundle full read-only state into one JSON
|
||
|
||
bank Bank-side data (Qonto + Wise) + Dolibarr reconciliation
|
||
probe Auth + discovery (org slug, profile id, balance ids)
|
||
qonto-transactions [--month|--since|--until] Qonto transactions table (incoming + outgoing)
|
||
wise-transactions [--month|--since|--until|--type|--enrich] Wise activities (incoming + outgoing)
|
||
match [--month|--since|--until|--window-days N|--enrich] Match bank ↔ Dolibarr (split buckets)
|
||
balance Live balances + Dolibarr cross-check per fk_account
|
||
curl <qonto|wise> <path> Raw read-only curl through bank-curl.sh
|
||
|
||
email Supplier-invoice emails from the Zoho mailbox
|
||
list [--folder|--limit|--candidates-only|--all-folders] List candidates
|
||
inspect <messageId> [--folder|--save-pdf|--json] Parse PDFs + draft Dolibarr entry
|
||
curl <path> Raw read-only curl through zoho-curl.sh
|
||
|
||
whoami GET /users/info — confirm auth
|
||
ping GET /status — liveness + Dolibarr version
|
||
curl <path> Raw read-only curl through dol-curl.sh
|
||
|
||
help [command] Show this help (or subcommand help)
|
||
|
||
CREDENTIALS
|
||
Reads .claude/skills/dolibarr/.env (mode 600, gitignored). See
|
||
.claude/skills/dolibarr/README.md for one-time setup.
|
||
|
||
EXAMPLES
|
||
arcodange ping
|
||
arcodange whoami
|
||
arcodange invoice list
|
||
arcodange invoice audit 12
|
||
arcodange tva summary --year 2026
|
||
arcodange thirdparty audit-all
|
||
arcodange snapshot --out /tmp/erp.json
|
||
EOF
|
||
}
|
||
|
||
# --- Dispatch ------------------------------------------------------------
|
||
|
||
cmd="${1:-help}"
|
||
shift || true
|
||
|
||
case "${cmd}" in
|
||
|
||
tva)
|
||
sub="${1:-help}"; shift || true
|
||
case "${sub}" in
|
||
collect) exec "${SKILLS}/dolibarr-tva-reconciliation/scripts/tva-by-month.sh" "$@" ;;
|
||
collect-detail) exec "${SKILLS}/dolibarr-tva-reconciliation/scripts/tva-line-detail.sh" "$@" ;;
|
||
deductible) exec "${SKILLS}/dolibarr-tva-deductible/scripts/deductible-by-month.sh" "$@" ;;
|
||
deductible-detail) exec "${SKILLS}/dolibarr-tva-deductible/scripts/deductible-line-detail.sh" "$@" ;;
|
||
summary) exec "${SKILLS}/dolibarr-tva-summary/scripts/tva-summary.sh" "$@" ;;
|
||
help|-h|--help)
|
||
cat <<'EOF'
|
||
arcodange tva — French TVA monthly preparation.
|
||
|
||
collect TVA collectée by month × rate (CA3 A1/A4/E2)
|
||
collect-detail Per-line audit, customer side
|
||
deductible TVA déductible by month × rate (CA3 19/20/17+24)
|
||
deductible-detail Per-line audit, supplier side
|
||
summary CA3-ready monthly net summary (collectée − déductible)
|
||
|
||
All accept --year YYYY, --since YYYY-MM-DD, --until YYYY-MM-DD.
|
||
EOF
|
||
;;
|
||
*) echo "arcodange tva: unknown subcommand '${sub}' (try 'arcodange tva help')" >&2; exit 2 ;;
|
||
esac
|
||
;;
|
||
|
||
invoice)
|
||
sub="${1:-help}"; shift || true
|
||
case "${sub}" in
|
||
list) exec "${SKILLS}/dolibarr-invoice-audit/scripts/list-km-invoices.sh" "$@" ;;
|
||
audit) exec "${SKILLS}/dolibarr-invoice-audit/scripts/audit-invoice.sh" "$@" ;;
|
||
help|-h|--help)
|
||
cat <<'EOF'
|
||
arcodange invoice — customer (KissMetrics) invoice operations.
|
||
|
||
list [--since YYYY-MM-DD] Table of KM invoices with payment state
|
||
audit <invoice-id> JSON facts + PDF mandatory-mention audit
|
||
EOF
|
||
;;
|
||
*) echo "arcodange invoice: unknown subcommand '${sub}' (try 'arcodange invoice help')" >&2; exit 2 ;;
|
||
esac
|
||
;;
|
||
|
||
thirdparty)
|
||
sub="${1:-help}"; shift || true
|
||
case "${sub}" in
|
||
audit) exec "${SKILLS}/dolibarr-thirdparty-completeness/scripts/audit-thirdparty.sh" "$@" ;;
|
||
audit-all) exec "${SKILLS}/dolibarr-thirdparty-completeness/scripts/audit-all-thirdparties.sh" "$@" ;;
|
||
help|-h|--help)
|
||
cat <<'EOF'
|
||
arcodange thirdparty — clients + suppliers completeness.
|
||
|
||
audit <socid> Country-aware audit for one thirdparty
|
||
audit-all [--clients-only|--suppliers-only] Audit every visible thirdparty
|
||
EOF
|
||
;;
|
||
*) echo "arcodange thirdparty: unknown subcommand '${sub}'" >&2; exit 2 ;;
|
||
esac
|
||
;;
|
||
|
||
payments)
|
||
sub="${1:-help}"; shift || true
|
||
case "${sub}" in
|
||
state) exec "${SKILLS}/dolibarr-payments-state/scripts/km-payment-state.sh" "$@" ;;
|
||
timeline) exec "${SKILLS}/dolibarr-payments-state/scripts/km-payment-timeline.sh" "$@" ;;
|
||
by-month) exec "${SKILLS}/dolibarr-payments-state/scripts/payments-by-month.sh" "$@" ;;
|
||
help|-h|--help)
|
||
cat <<'EOF'
|
||
arcodange payments — KissMetrics cash-receipt tracking.
|
||
|
||
state [--since YYYY-MM-DD] Per-invoice TTC vs payments reconciliation
|
||
timeline [--year|--since|--until] Payment timeline with cumulative balance
|
||
by-month [--year|--all-clients] Monthly aggregation
|
||
EOF
|
||
;;
|
||
*) echo "arcodange payments: unknown subcommand '${sub}'" >&2; exit 2 ;;
|
||
esac
|
||
;;
|
||
|
||
templates)
|
||
sub="${1:-help}"; shift || true
|
||
case "${sub}" in
|
||
list) exec "${SKILLS}/dolibarr-recurring-templates/scripts/list-templates.sh" "$@" ;;
|
||
inspect) exec "${SKILLS}/dolibarr-recurring-templates/scripts/inspect-template.sh" "$@" ;;
|
||
help|-h|--help)
|
||
cat <<'EOF'
|
||
arcodange templates — recurring invoice templates.
|
||
|
||
list [--max-id N] Enumerate templates (probes ids)
|
||
inspect <template-id> Full audit with health checks
|
||
EOF
|
||
;;
|
||
*) echo "arcodange templates: unknown subcommand '${sub}'" >&2; exit 2 ;;
|
||
esac
|
||
;;
|
||
|
||
snapshot)
|
||
exec "${SKILLS}/dolibarr-data-snapshot/scripts/snapshot.sh" "$@"
|
||
;;
|
||
|
||
bank)
|
||
sub="${1:-help}"; shift || true
|
||
case "${sub}" in
|
||
probe) exec "${SKILLS}/arcodange-bank-reco/scripts/bank-probe.sh" "$@" ;;
|
||
qonto-transactions) exec "${SKILLS}/arcodange-bank-reco/scripts/qonto-transactions.sh" "$@" ;;
|
||
wise-transactions) exec "${SKILLS}/arcodange-bank-reco/scripts/wise-transactions.sh" "$@" ;;
|
||
match) exec "${SKILLS}/arcodange-bank-reco/scripts/bank-match.sh" "$@" ;;
|
||
balance) exec "${SKILLS}/arcodange-bank-reco/scripts/bank-balance.sh" "$@" ;;
|
||
curl)
|
||
if [[ $# -lt 2 ]]; then
|
||
echo "arcodange bank curl: usage: bank curl <qonto|wise> <path>" >&2; exit 2
|
||
fi
|
||
exec "${SKILLS}/arcodange-bank-reco/scripts/bank-curl.sh" "$@"
|
||
;;
|
||
help|-h|--help)
|
||
cat <<'EOF'
|
||
arcodange bank — bank-side data (Qonto + Wise) and Dolibarr reconciliation.
|
||
|
||
probe Auth + discovery (org slug, profile id, balance ids)
|
||
qonto-transactions [--month|--since|--until] Qonto transactions table
|
||
wise-transactions [--month|--since|--until|--type|--enrich] Wise activities (incoming + outgoing)
|
||
match [--month|--since|--until|--window-days N] Match bank ↔ Dolibarr (3 buckets)
|
||
balance Live balances + Dolibarr cross-check per fk_account
|
||
curl <qonto|wise> <path> Raw read-only curl through bank-curl.sh
|
||
|
||
Requires QONTO_LOGIN, QONTO_SECRET_KEY, QONTO_ORG_SLUG, WISE_API_TOKEN,
|
||
WISE_PROFILE_ID in .env. See arcodange-bank-reco/SKILL.md for setup.
|
||
EOF
|
||
;;
|
||
*) echo "arcodange bank: unknown subcommand '${sub}' (try 'arcodange bank help')" >&2; exit 2 ;;
|
||
esac
|
||
;;
|
||
|
||
email)
|
||
sub="${1:-help}"; shift || true
|
||
case "${sub}" in
|
||
list) exec "${SKILLS}/arcodange-email-ingest/scripts/email-list.sh" "$@" ;;
|
||
inspect) exec "${SKILLS}/arcodange-email-ingest/scripts/email-inspect.sh" "$@" ;;
|
||
curl) exec "${SKILLS}/arcodange-email-ingest/scripts/zoho-curl.sh" "$@" ;;
|
||
help|-h|--help)
|
||
cat <<'EOF'
|
||
arcodange email — supplier-invoice ingestion from the Zoho mailbox.
|
||
|
||
list [--folder PATH|--limit N|--candidates-only|--all-folders]
|
||
List messages (default: /Inbox/books)
|
||
inspect <messageId> [--folder PATH|--save-pdf DIR|--json]
|
||
Parse PDF attachments, propose Dolibarr supplier-invoice draft
|
||
curl <path> Raw read-only call through zoho-curl.sh
|
||
|
||
Requires ZOHO_CLIENT_ID, ZOHO_CLIENT_SECRET, ZOHO_REFRESH_TOKEN, ZOHO_DC in .env.
|
||
See arcodange-email-ingest/SKILL.md for OAuth setup.
|
||
EOF
|
||
;;
|
||
*) echo "arcodange email: unknown subcommand '${sub}' (try 'arcodange email help')" >&2; exit 2 ;;
|
||
esac
|
||
;;
|
||
|
||
whoami)
|
||
exec "${DOLC}" /users/info
|
||
;;
|
||
|
||
ping)
|
||
exec "${DOLC}" /status
|
||
;;
|
||
|
||
curl)
|
||
if [[ $# -lt 1 ]]; then
|
||
echo "arcodange curl: missing path. Example: arcodange curl /invoices/12" >&2
|
||
exit 2
|
||
fi
|
||
exec "${DOLC}" "$@"
|
||
;;
|
||
|
||
help|-h|--help|"")
|
||
if [[ $# -gt 0 ]]; then
|
||
exec "$0" "$1" help
|
||
fi
|
||
usage
|
||
;;
|
||
|
||
*)
|
||
echo "arcodange: unknown command '${cmd}'" >&2
|
||
echo " Try 'arcodange help' for the list." >&2
|
||
exit 2
|
||
;;
|
||
esac
|