Files
erp/.claude/skills/dolibarr/SKILL.md
Gabriel Radureau c2d8479f5e add arcodange-email-ingest — Zoho Mail → Dolibarr supplier-invoice drafts
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>
2026-05-31 14:56:15 +02:00

13 KiB

name, description, requires
name description requires
dolibarr Reference + connection layer for the Arcodange Dolibarr ERP REST API at https://erp.arcodange.lab/. Documents authentication via DOLAPIKEY, the read-only `ai_agent` account's permission requirements (the `voir_tous` ACL trap that returns empty arrays or 404s instead of 403s on listing endpoints), the credentials layout at `.claude/skills/dolibarr/.env`, the `scripts/dol-curl.sh` wrapper, the catalogue of useful read endpoints (invoices, thirdparties, documents/download, products, status, users/info), and the common gotchas (mode-as-int, paye-vs-status, empty-vs-404-vs-403, unix-epoch dates). Use when setting up the connection, debugging an API call, looking up an endpoint, decoding a permission error, or as a dependency referenced by any `dolibarr-*` workflow skill. SKIP for Dolibarr admin / Helm chart / Ansible config (covered by `chart/` + `ansible/`), for any ERP other than Arcodange's, and for specific business workflows that have their own `dolibarr-<topic>` skill.
bins auth
curl
jq
python3
true

dolibarr — Arcodange ERP API platform reference

This is the base skill for talking to the Arcodange Dolibarr instance from Claude Code. It documents the connection, the permissions, the endpoint catalogue, and the gotchas. Each business workflow lives in its own dolibarr-<topic> sibling skill that depends on this one — V1 ships dolibarr-invoice-audit for the KissMetrics audit; future siblings will follow the same pattern (e.g. dolibarr-payments-state, dolibarr-tva-reconciliation).

The API key for ai_agent is read-only by design. Never attempt writes from any dolibarr-* skill — use the Dolibarr UI directly for corrections.

Quick start

From the repo root:

bin/arcodange whoami | jq -r .login           # → ai_agent
bin/arcodange ping                            # → {"success":{"code":200,"dolibarr_version":"22.0.4",...}}
bin/arcodange curl /invoices/12               # raw read-only call

The bin/arcodange CLI is the human entry point; under the hood it delegates to the same per-skill scripts. Direct script access still works:

./.claude/skills/dolibarr/scripts/dol-curl.sh /users/info | jq -r .login

If you see ai_agent, auth works. If not, check .env (next section) and dol-curl.sh's error message.

For a liveness check that needs no auth:

curl -s https://erp.arcodange.lab/api/index.php/status | jq .
# → {"success":{"code":200,"dolibarr_version":"22.0.4",...}}

Credentials

Schema of .claude/skills/dolibarr/.env (mode 600, gitignored):

DOLIBARR_URL=https://erp.arcodange.lab
DOLIBARR_API_KEY=<rotatable api key for ai_agent>
DOLIBARR_USER=ai_agent
DOLIBARR_PASSWORD=<used only for browser/UI login, very rarely>

The DOLAPIKEY is the only secret needed for API calls. DOLIBARR_USER/DOLIBARR_PASSWORD are kept here only so the operator has them at hand if they need to log into the Dolibarr UI to fix permissions.

Rules:

  • chmod 600 .env. Verify with stat -f %Lp .env600.
  • Never commit. The root .gitignore already excludes .env at any depth; redundant patterns (*.credentials, .claude/skills/**/.env) belt-and-braces it.
  • Never paste the key into chat. If a key leaks, rotate it from the Dolibarr UI: Users → ai_agent → API key → Generate new.
  • Every sibling dolibarr-* skill sources this file via ../dolibarr/.env. There is exactly one copy.

Permission gotcha — read this first

Dolibarr enforces TWO layers of access:

  1. Per-API-endpoint permissions (does the user's role allow /invoices?).
  2. Per-record ACL (can this user see THIS invoice/thirdparty?).

Layer 2 is the trap. When ai_agent has the endpoint permission but not the voir_tous flag, listing endpoints silently filter rows out:

  • GET /invoices returns [] with HTTP 200 (looks like "no invoices exist").
  • GET /thirdparties returns HTTP 404 with "No third parties found" (looks like "no thirdparties exist").
  • GET /thirdparties/{id} returns HTTP 403 with "Access not allowed for login ai_agent on this thirdparty" (this is the only honest error).

Diagnostic recipe. If a listing endpoint returns empty/404 while you expect data:

./scripts/dol-curl.sh /thirdparties/1

If you get a 403 with Access not allowed, the ACL is the cause (saved as a reference at examples/acl_403_thirdparty.json). Do not retry the call blindly. Fix the permissions, then retest.

Fix. In the Dolibarr UI (https://erp.arcodange.lab/ → Setup → Users & Groups → ai_agent → Permissions), tick:

  • Tiers → Lire les tiers
  • Tiers → Voir tous les tiers (et pas seulement ceux liés à l'utilisateur courant)
  • Factures → Lire les factures
  • Factures → Voir toutes les factures (et pas seulement celles liées à l'utilisateur courant)

Add the equivalent flags for any new module a future dolibarr-* skill needs (Paiements, Produits, …). The ai_agent user has no UI access by default, so voir_tous is the right grant — there's no sales-rep relationship to honor.

Endpoint catalogue

Read-only endpoints we've validated against this instance. Live captures are under examples/.

Endpoint HTTP Purpose Key params / gotchas Example
GET /status 200 Liveness + Dolibarr version No auth needed status.json
GET /users/info 200 Current API user (auth sanity) Returns login, id, rights tree users_info.json
GET /invoices 200 List invoices limit, sortfield=t.datef, sortorder=DESC, status=draft|unpaid|paid, sqlfilters invoices_list.json
GET /invoices/{id} 200 Invoice detail paye=1 is the canonical "paid" flag invoice_detail.json
GET /invoices/{id}/payments 200 Payments applied to one invoice Returns array of {amount, type, date, num, ref, fk_bank_line}. date is a string YYYY-MM-DD HH:MM:SS, NOT unix epoch like elsewhere. Amounts negative for AVOIRs. invoices_12_payments.json
GET /payments 501 (list-all not implemented) Returns "API not found". To enumerate payments, iterate invoices.
GET /bankaccounts / /{id} 200 List bank accounts / detail Returns ref, label, iban, country_code. fk_bank_line on a payment doesn't directly map to the account id — see dolibarr-payments-state skill for the lookup. bankaccounts_list.json
GET /thirdparties 200 List thirdparties mode MUST be integer (mode=1 = customers). String fails 400. (use /{id} below)
GET /thirdparties/{id} 200 Thirdparty detail KissMetrics is id=1 in this instance thirdparty_km.json
GET /products / /products/{id} 200 Product catalogue KM-audit is id=1, KM-cloud-devops is id=2 products_list.json
GET /supplierinvoices / /{id} 200 Supplier (fournisseur) invoices Lines are included inline on the detail endpoint (same shape as /invoices/{id}). Sub-endpoint /lines returns HTTP 403 for ai_agent — not needed since inline lines work. supplierinvoices_list.json · supplierinvoice_detail.json
GET /invoices/templates/{id} 200 Recurring invoice template detail No list endpoint — probe ids 1..N. Empty ids return HTTP 200 with id=null (sentinel for "doesn't exist"). Fields: ref, socid, frequency, unit_frequency, nb_gen_done, date_when, date_last_gen, suspended, auto_validate, lines. invoice_template_km.json
GET /documents/download 200 Download a stored document as base64 modulepart=facture&original_file=<REF>/<REF>.pdf (URL-encode slashes). Returns {filename, content-type, filesize, content} with content base64. document_download_meta.json

Not available on this account (intentionally): /setup/modules (admin-only), /invoices/templates (requires a path arg — different route shape; V2).

The dol-curl.sh wrapper

scripts/dol-curl.sh is a thin wrapper used by this skill and every sibling. It:

  • Sources ../.env (relative to itself), so it works regardless of the caller's CWD.
  • Fails loudly if .env is missing or DOLIBARR_URL / DOLIBARR_API_KEY is unset.
  • Prepends DOLAPIKEY and Accept: application/json headers + a 30s timeout.
  • Prints the response body to stdout.
  • On HTTP ≥ 400, also prints dol-curl.sh: HTTP <code> on <path> to stderr and exits 1 — so set -e in caller scripts surfaces the failure.
# Examples
./scripts/dol-curl.sh /users/info | jq .login
./scripts/dol-curl.sh '/invoices?limit=5&sortfield=t.datef&sortorder=DESC' | jq '.[].ref'
./scripts/dol-curl.sh /invoices/12 | jq '{ref, paye, total_ht, last_main_doc}'
./scripts/dol-curl.sh '/documents/download?modulepart=facture&original_file=FAC002-CL0001002%2FFAC002-CL0001002.pdf' \
  | jq -r .content | base64 -d > /tmp/inv.pdf

Common gotchas

  • mode is an integer. ?mode=1 works; ?mode=customer → HTTP 400 Invalid value specified for mode.
  • Empty list vs 404 vs 403. /invoices returns [] (200) on empty/permission-filtered. /thirdparties returns 404 "No third parties found". /thirdparties/{id} returns 403 if the ACL hides the row. All three can mean "no permission" — the diagnostic is to hit /thirdparties/1 directly (see Permission gotcha above).
  • Dates are usually unix epoch (seconds) on most objects: .date, .datef, .date_lim_reglement, .tms are integers.
    python3 -c "import datetime,sys; print(datetime.datetime.fromtimestamp(int(sys.argv[1])))" 1771887600
    # → 2026-02-24 00:00:00
    
    Exception: /invoices/{id}/payments returns .date as the string "YYYY-MM-DD HH:MM:SS", not an int. Check the field type before parsing.
  • paye is the paid flag, status is the workflow state. status=2 means "validated", paye=1 means "fully paid". They're independent — a status-2 invoice can still be unpaid.
  • last_main_doc paths are relative to Dolibarr's document root. For /documents/download, pass modulepart=facture + URL-encoded original_file=<dir>/<file> (encode the /).
  • array_options=[] vs {}. Dolibarr returns [] when no extrafields are set and {} when there are some. Treat both as "no custom fields" in Python.

Pointers

Out of scope

  • Writes. The ai_agent key is read-only by design. For corrections (edit a thirdparty, void an invoice, fix a mention), use the Dolibarr UI.
  • Admin endpoints. /setup/* is gated to admins or to logins listed in API_LOGINS_ALLOWED_FOR_GET_MODULES; not available to ai_agent.
  • Helm chart / Ansible / Kubernetes operations. Covered by chart/, ansible/, and the project root README.md.