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>
162 lines
13 KiB
Markdown
162 lines
13 KiB
Markdown
---
|
|
name: dolibarr
|
|
description: 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.
|
|
requires:
|
|
bins: ["curl", "jq", "python3"]
|
|
auth: 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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
./.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:
|
|
|
|
```bash
|
|
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 .env` → `600`.
|
|
- 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*:
|
|
|
|
```bash
|
|
./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](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/](examples/).
|
|
|
|
| Endpoint | HTTP | Purpose | Key params / gotchas | Example |
|
|
|------------------------------------------------|------|--------------------------------------|------------------------------------------------------------------|---------|
|
|
| `GET /status` | 200 | Liveness + Dolibarr version | No auth needed | [status.json](examples/status.json) |
|
|
| `GET /users/info` | 200 | Current API user (auth sanity) | Returns `login`, `id`, `rights` tree | [users_info.json](examples/users_info.json) |
|
|
| `GET /invoices` | 200 | List invoices | `limit`, `sortfield=t.datef`, `sortorder=DESC`, `status=draft\|unpaid\|paid`, `sqlfilters` | [invoices_list.json](examples/invoices_list.json) |
|
|
| `GET /invoices/{id}` | 200 | Invoice detail | `paye=1` is the canonical "paid" flag | [invoice_detail.json](examples/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](examples/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](examples/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](examples/thirdparty_km.json) |
|
|
| `GET /products` / `/products/{id}` | 200 | Product catalogue | `KM-audit` is `id=1`, `KM-cloud-devops` is `id=2` | [products_list.json](examples/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](examples/supplierinvoices_list.json) · [supplierinvoice_detail.json](examples/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](examples/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](examples/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](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.
|
|
|
|
```bash
|
|
# 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.
|
|
```bash
|
|
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
|
|
|
|
- Workflow skill for invoice + thirdparty audits: [dolibarr-invoice-audit](../dolibarr-invoice-audit/SKILL.md).
|
|
- Workflow skill for payment-state and cash-receipt tracking: [dolibarr-payments-state](../dolibarr-payments-state/SKILL.md).
|
|
- Workflow skill for monthly TVA basis (CA3 / CA12 preparation): [dolibarr-tva-reconciliation](../dolibarr-tva-reconciliation/SKILL.md).
|
|
- Workflow skill for recurring invoice templates: [dolibarr-recurring-templates](../dolibarr-recurring-templates/SKILL.md).
|
|
- Workflow skill for point-in-time state archival: [dolibarr-data-snapshot](../dolibarr-data-snapshot/SKILL.md).
|
|
- Workflow skill for thirdparty completeness audit (any client / supplier): [dolibarr-thirdparty-completeness](../dolibarr-thirdparty-completeness/SKILL.md).
|
|
- Workflow skill for supplier-side TVA déductible (CA3 lignes 19 / 20 / 17+24): [dolibarr-tva-deductible](../dolibarr-tva-deductible/SKILL.md).
|
|
- Workflow skill for composite CA3-ready TVA summary (collectée + déductible + net): [dolibarr-tva-summary](../dolibarr-tva-summary/SKILL.md).
|
|
- **Bank-side reconciliation** (Qonto + Wise ↔ Dolibarr matching): [arcodange-bank-reco](../arcodange-bank-reco/SKILL.md).
|
|
- **Email ingestion** (Zoho Mail → supplier-invoice draft for Dolibarr): [arcodange-email-ingest](../arcodange-email-ingest/SKILL.md).
|
|
- Future workflow skills follow the `dolibarr-<topic>` convention (ERP-internal) or `arcodange-<topic>` (cross-system). Each one depends on this skill for connection + permissions + endpoint reference; each one keeps its triggers focused on its specific business workflow.
|
|
|
|
## 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`.
|