add dolibarr api skills for read-only inspection #1
189
.claude/skills/dolibarr-invoice-audit/SKILL.md
Normal file
189
.claude/skills/dolibarr-invoice-audit/SKILL.md
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
---
|
||||||
|
name: dolibarr-invoice-audit
|
||||||
|
description: Audit Arcodange invoices and the KissMetrics thirdparty record against contractual and French legal expectations. Three workflows — (1) list KissMetrics invoices with payment state and flag credit notes (AVC refs or negative HT); (2) audit one invoice end-to-end — JSON side (socid / dates / HT / TVA reverse charge / paye flag) AND PDF side (structural presence of mandatory mentions: forme juridique, SIRET, numéro TVA intracom, RCS Évry, NAF-APE, capital social, TVA 259-1° CGI, L.441-10 penalties at BCE+10 or 12,15 %, 40 € indemnité forfaitaire, L.123-22 / R.123-237); (3) audit the KissMetrics thirdparty record for address completeness and EIN presence (today idprof1..6 are empty — known gap to surface). Use when the user asks "is the M1 invoice emitted", "is invoice X paid", "vérifier les mentions obligatoires", "audit mentions légales", "état des paiements KissMetrics", "vérifier la fiche KissMetrics", or any cohort-review billing check on the Arcodange Dolibarr. Depends on the `dolibarr` skill for connection + permissions. SKIP for non-Arcodange ERPs, for non-KissMetrics clients (future skill), for write/correction tasks (the API is read-only — use the Dolibarr UI), and for Helm / Ansible deployment work.
|
||||||
|
requires:
|
||||||
|
bins: ["curl", "jq", "python3", "pdftotext"]
|
||||||
|
auth: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# dolibarr-invoice-audit — KissMetrics billing audit
|
||||||
|
|
||||||
|
This skill answers the questions the Arcodange × KissMetrics cohort review keeps asking:
|
||||||
|
|
||||||
|
- Has the M-N invoice been emitted? Is it paid?
|
||||||
|
- Does the PDF carry the mandatory French legal mentions?
|
||||||
|
- Is the KissMetrics thirdparty record complete enough (EIN, full address)?
|
||||||
|
|
||||||
|
It depends on the [dolibarr](../dolibarr/SKILL.md) base skill for the connection, the `.env` credentials, and the `voir_tous` permission setup. **If `list-km-invoices.sh` returns nothing, your first stop is the permission gotcha in the base skill — not this one.**
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- [`dolibarr/.env`](../dolibarr/.env) populated with a valid `DOLIBARR_API_KEY`, mode 600.
|
||||||
|
- `ai_agent` in Dolibarr has the four `voir_tous` flags ticked (see [dolibarr/README.md](../dolibarr/README.md) step 2).
|
||||||
|
- `pdftotext` installed locally: `brew install poppler`.
|
||||||
|
|
||||||
|
All scripts source the base skill's `.env` via the relative path `../../dolibarr/scripts/dol-curl.sh`. Run them from anywhere.
|
||||||
|
|
||||||
|
## Workflow 1 — List KissMetrics invoices
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/list-km-invoices.sh # all KM invoices, newest first
|
||||||
|
./scripts/list-km-invoices.sh --since 2026-04-01 # filter by invoice date
|
||||||
|
```
|
||||||
|
|
||||||
|
Output (live as of 2026-05-28, captured at [examples/list-km-invoices.txt](examples/list-km-invoices.txt)):
|
||||||
|
|
||||||
|
```
|
||||||
|
id ref date HT TVA TTC paid AVC pdf
|
||||||
|
----------------------------------------------------------------------------------------------------------------
|
||||||
|
12 FAC002-CL0001002 2026-02-24 5100.00 0.00 5100.00 yes facture/FAC002-CL0001002/FAC002-CL0001002.pdf
|
||||||
|
13 FAC003-CL0001003 2026-02-24 2550.00 0.00 2550.00 yes facture/FAC003-CL0001003/FAC003-CL0001003.pdf
|
||||||
|
2 FAC001-CL00001 2026-01-30 510.00 0.00 510.00 yes facture/FAC001-CL00001/FAC001-CL00001.pdf
|
||||||
|
11 FAC001-CL0001001 2026-01-30 510.00 0.00 510.00 yes facture/FAC001-CL0001001/FAC001-CL0001001.pdf
|
||||||
|
10 AVC001-CL0001001 2026-01-30 -510.00 0.00 -510.00 yes AVOIR facture/AVC001-CL0001001/AVC001-CL0001001.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
Read the columns:
|
||||||
|
- **paid** — Dolibarr's `paye=1` flag. `yes` means fully paid (not just "validated").
|
||||||
|
- **AVC** — `AVOIR` is shown when the ref starts with `AVC` *or* the HT is negative. The two should match; a discrepancy is a Dolibarr data issue.
|
||||||
|
- **pdf** — `last_main_doc`. Pass to `audit-invoice.sh` indirectly via the invoice id.
|
||||||
|
|
||||||
|
KissMetrics is the thirdparty with `socid=1` in this Dolibarr. If that ever changes, update `KM_SOCID` in the script. The TOTAL line sums HT/TTC across all visible rows (including AVOIRs, which subtract — handy for net revenue checks).
|
||||||
|
|
||||||
|
## Workflow 2 — Audit one invoice (JSON facts + PDF mandatory mentions)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/audit-invoice.sh 12 # FAC002-CL0001002 (current M1 candidate)
|
||||||
|
echo "exit: $?" # 0 if every mention check passes, 1 otherwise
|
||||||
|
```
|
||||||
|
|
||||||
|
The script does two passes against `/invoices/{id}` and the attached PDF.
|
||||||
|
|
||||||
|
### JSON side
|
||||||
|
|
||||||
|
Prints the structured facts the cohort review needs to spot-check against the contract:
|
||||||
|
|
||||||
|
```
|
||||||
|
Thirdparty : KissMetrics (socid=1)
|
||||||
|
Invoice date : 2026-02-24
|
||||||
|
Due date : 2026-03-31 (cond_reglement_code=10DENDMONTH)
|
||||||
|
Total HT : 5100.00000000
|
||||||
|
Total TVA : 0.00000000 (TVA=0 → reverse-charge expected; the 259-1° CGI mention MUST be on the PDF)
|
||||||
|
Total TTC : 5100.00000000
|
||||||
|
Payment state : PAID
|
||||||
|
PDF : facture/FAC002-CL0001002/FAC002-CL0001002.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
Cross-check against the cohort spec:
|
||||||
|
- `socid` matches `KissMetrics` (id=1)
|
||||||
|
- `date` is in the expected billing month
|
||||||
|
- `total_ht` matches the contract line (10 days × 510 €/d = 5 100 € for FAC002, etc.)
|
||||||
|
- `total_tva == 0` confirms the reverse-charge posture — the 259-1° CGI mention is then **mandatory** on the PDF
|
||||||
|
- `paye == 1` if the invoice should be paid by now
|
||||||
|
- `cond_reglement_code` tells you the payment-term shape (`10DENDMONTH` = "10 jours fin de mois")
|
||||||
|
|
||||||
|
### PDF side — mandatory-mention audit
|
||||||
|
|
||||||
|
Downloads the PDF via `/documents/download`, base64-decodes, runs `pdftotext -layout`, then greps for **structural presence** of each mandatory mention. We deliberately do NOT match against the placeholder values in `static/config/company.json` — that file is a template; the real legal identifiers (SIRET `99965745500013`, TVA `FR00999657455`, forme **SARL**) live in Dolibarr's own setup and are what gets rendered into the PDF.
|
||||||
|
|
||||||
|
The checklist:
|
||||||
|
|
||||||
|
| Mention | Why it must be there |
|
||||||
|
|--------------------------------------|----------------------------------------------|
|
||||||
|
| Forme juridique (SARL / SAS / EURL / SA / SCI / SASU) | L.123-12 du Code de commerce (forme + capital) |
|
||||||
|
| SIRET (14 digits) | Décret 2003-1196 |
|
||||||
|
| Numéro TVA intracom (FR + 11) | Article 242 nonies A annexe II du CGI |
|
||||||
|
| RCS / R.C.S. Évry | L.123-12 (identification au registre) |
|
||||||
|
| NAF-APE code | Décret 2003-1196 |
|
||||||
|
| Capital social | L.123-12 du Code de commerce |
|
||||||
|
| TVA 259-1° CGI (reverse charge) | Article 259-1° CGI for services to non-EU B2B |
|
||||||
|
| L.441-10 — BCE+10 or 12,15 % | Code de commerce L.441-10 (late-payment penalties) |
|
||||||
|
| 40 € indemnité forfaitaire | Décret 2012-1115 (recouvrement) |
|
||||||
|
| L.123-22 / R.123-237 | Identification supplementaire des actes commerciaux |
|
||||||
|
|
||||||
|
Live result on invoice 12 (captured at [examples/audit-invoice-12.txt](examples/audit-invoice-12.txt) — exit code 1):
|
||||||
|
|
||||||
|
```
|
||||||
|
[OK] Forme juridique (SARL/SAS/EURL/SA/SCI/SASU)
|
||||||
|
[OK] SIRET (14 digits)
|
||||||
|
[OK] Numéro TVA intracom (FR + 11 chars)
|
||||||
|
[OK] RCS / R.C.S. Évry
|
||||||
|
[OK] NAF-APE code
|
||||||
|
[XX] Capital social
|
||||||
|
[OK] TVA 259-1° CGI / autoliquidation
|
||||||
|
[XX] L.441-10 — BCE+10 or 12,15 %
|
||||||
|
[XX] 40 € indemnité forfaitaire
|
||||||
|
[XX] L.123-22 / R.123-237
|
||||||
|
|
||||||
|
Real identifiers extracted from the PDF (informational):
|
||||||
|
SARL
|
||||||
|
SIRET: 99965745500013
|
||||||
|
TVA: FR00999657455
|
||||||
|
APE: 6201Z
|
||||||
|
|
||||||
|
6 pass / 4 fail
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a **real cohort-review finding**: the four `[XX]` lines are mandatory mentions absent from the live Dolibarr PDF template. To fix them, edit the Dolibarr invoice template (Setup → PDF model → Edit) and re-issue an avoir + new invoice. The skill itself doesn't touch the template — it just measures.
|
||||||
|
|
||||||
|
Also note: `static/config/company.json` claims forme juridique = SAS but the real Dolibarr says **SARL**. The PDF is the source of truth; the `static/config/company.json` file appears to hold placeholders.
|
||||||
|
|
||||||
|
## Workflow 3 — Audit the KissMetrics thirdparty record
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/audit-km-thirdparty.sh
|
||||||
|
echo "exit: $?" # 0 if complete, 1 otherwise
|
||||||
|
```
|
||||||
|
|
||||||
|
Live result (captured at [examples/audit-km-thirdparty.txt](examples/audit-km-thirdparty.txt) — exit code 1):
|
||||||
|
|
||||||
|
```
|
||||||
|
[OK] name = 'KissMetrics'
|
||||||
|
[OK] client flag = 'yes'
|
||||||
|
[OK] country_code = 'US'
|
||||||
|
[OK] state_id = '1167'
|
||||||
|
[OK] address = '2850 34th Street North, 307'
|
||||||
|
[OK] zip = '33713'
|
||||||
|
[OK] town = 'St. Petersburg'
|
||||||
|
[OK] email = 'evan@kissmetrics.io'
|
||||||
|
[XX] phone = None
|
||||||
|
[XX] url = None
|
||||||
|
|
||||||
|
idprof1..idprof6 (EIN slot for non-EU customers):
|
||||||
|
idprof1 = '' ... idprof6 = ''
|
||||||
|
[XX] EIN present (any idprof populated)
|
||||||
|
```
|
||||||
|
|
||||||
|
Open follow-ups for the cohort review:
|
||||||
|
- **EIN missing** — all `idprof1..6` empty. KissMetrics is a US entity; the EIN should land in `idprof1` or `idprof2` per Dolibarr convention. Either fill it in the Dolibarr UI or document why it's intentionally absent.
|
||||||
|
- **Phone / URL missing** — minor; not legally required for B2B invoicing but useful for the customer file.
|
||||||
|
|
||||||
|
## Captured baselines
|
||||||
|
|
||||||
|
[`examples/`](examples/) holds the live outputs of each script as of the V1 baseline (2026-05-28). To detect drift, re-run a script and `diff` against the captured file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/audit-invoice.sh 12 > /tmp/new.txt 2>&1
|
||||||
|
diff examples/audit-invoice-12.txt /tmp/new.txt && echo "unchanged"
|
||||||
|
```
|
||||||
|
|
||||||
|
When you intentionally update the audit (new mention, fixed regex, etc.), re-capture the baselines as part of the same change.
|
||||||
|
|
||||||
|
## Adding a new mandatory mention
|
||||||
|
|
||||||
|
In [`scripts/audit-invoice.sh`](scripts/audit-invoice.sh) find the block under `# Mandatory-mention audit — structural presence on the PDF:` and add:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
check "Your new mention label" "your-extended-regex-pattern"
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `[[:space:]]`, `[Cc]apital`-style character classes, and prefer matching presence of the law reference (`L\\.?441-10`) over matching the verbatim French text — the wording in the PDF template can drift.
|
||||||
|
|
||||||
|
If the new check should source a value from somewhere (e.g. company data), pull it via `jq` from a stable file (don't re-parse `static/config/company.json` — its values are placeholders). For Dolibarr-derived expectations, fetch them via `dol-curl.sh` at script start.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- **Writes / corrections.** The API key is read-only. To fix a missing mention or a wrong identifier, edit the Dolibarr invoice template / company setup in the UI and re-issue.
|
||||||
|
- **Audits for clients other than KissMetrics.** The scripts hard-code `KM_SOCID=1`. A future `dolibarr-invoice-audit-<client>` (or a parametrized rewrite) will handle other thirdparties.
|
||||||
|
- **Recurring-template inspection** (the `Kiss Metrics Invoice` template that backs FAC002 / FAC003). Different endpoint shape (`/invoices/templates`), V2 work.
|
||||||
|
- **Payment-state cross-reference with bank statements.** Future skill (likely `dolibarr-payments-state`).
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
================================================================================
|
||||||
|
Invoice 12 — FAC002-CL0001002
|
||||||
|
================================================================================
|
||||||
|
Thirdparty : KissMetrics (socid=1)
|
||||||
|
Invoice date : 2026-02-24
|
||||||
|
Due date : 2026-03-31 (cond_reglement_code=10DENDMONTH)
|
||||||
|
Total HT : 5100.00000000
|
||||||
|
Total TVA : 0.00000000 (TVA=0 → reverse-charge expected; the 259-1° CGI mention MUST be on the PDF)
|
||||||
|
Total TTC : 5100.00000000
|
||||||
|
Payment state : PAID
|
||||||
|
PDF : facture/FAC002-CL0001002/FAC002-CL0001002.pdf
|
||||||
|
|
||||||
|
Mandatory-mention audit — structural presence on the PDF:
|
||||||
|
[OK] Forme juridique (SARL/SAS/EURL/SA/SCI/SASU)
|
||||||
|
[OK] SIRET (14 digits)
|
||||||
|
[OK] Numéro TVA intracom (FR + 11 chars)
|
||||||
|
[OK] RCS / R.C.S. Évry
|
||||||
|
[OK] NAF-APE code
|
||||||
|
[XX] Capital social (looked for: [Cc]apital)
|
||||||
|
[OK] TVA 259-1° CGI / autoliquidation
|
||||||
|
[XX] L.441-10 — BCE+10 or 12,15 % (looked for: (L\.?441-10|BCE[[:space:]]*\+[[:space:]]*10|12[.,]15[[:space:]]*%))
|
||||||
|
[XX] 40 € indemnité forfaitaire (looked for: (40[[:space:]]*€|indemnit[eé] forfaitaire|D[eé]cret 2012-1115))
|
||||||
|
[XX] L.123-22 / R.123-237 (looked for: (L\.?123-22|R\.?123-237))
|
||||||
|
|
||||||
|
Real identifiers extracted from the PDF (informational):
|
||||||
|
SARL
|
||||||
|
SIRET: 99965745500013
|
||||||
|
TVA: FR00999657455
|
||||||
|
APE: 6201Z
|
||||||
|
|
||||||
|
6 pass / 4 fail
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
================================================================================
|
||||||
|
KissMetrics thirdparty audit — socid=1
|
||||||
|
================================================================================
|
||||||
|
[OK] name = 'KissMetrics'
|
||||||
|
[OK] client flag = 'yes'
|
||||||
|
[OK] country_code = 'US'
|
||||||
|
[OK] state_id = '1167'
|
||||||
|
[OK] address = '2850 34th Street North, 307'
|
||||||
|
[OK] zip = '33713'
|
||||||
|
[OK] town = 'St. Petersburg'
|
||||||
|
[OK] email = 'evan@kissmetrics.io'
|
||||||
|
[XX] phone = None
|
||||||
|
[XX] url = None
|
||||||
|
|
||||||
|
idprof1..idprof6 (EIN slot for non-EU customers):
|
||||||
|
idprof1 = ''
|
||||||
|
idprof2 = ''
|
||||||
|
idprof3 = ''
|
||||||
|
idprof4 = ''
|
||||||
|
idprof5 = ''
|
||||||
|
idprof6 = ''
|
||||||
|
[XX] EIN present (any idprof populated)
|
||||||
|
|
||||||
|
array_options (Dolibarr extrafields): (none)
|
||||||
|
|
||||||
|
8 pass / 3 fail
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
id ref date HT TVA TTC paid AVC pdf
|
||||||
|
----------------------------------------------------------------------------------------------------------------
|
||||||
|
12 FAC002-CL0001002 2026-02-24 5100.00 0.00 5100.00 yes facture/FAC002-CL0001002/FAC002-CL0001002.pdf
|
||||||
|
13 FAC003-CL0001003 2026-02-24 2550.00 0.00 2550.00 yes facture/FAC003-CL0001003/FAC003-CL0001003.pdf
|
||||||
|
2 FAC001-CL00001 2026-01-30 510.00 0.00 510.00 yes facture/FAC001-CL00001/FAC001-CL00001.pdf
|
||||||
|
11 FAC001-CL0001001 2026-01-30 510.00 0.00 510.00 yes facture/FAC001-CL0001001/FAC001-CL0001001.pdf
|
||||||
|
10 AVC001-CL0001001 2026-01-30 -510.00 0.00 -510.00 yes AVOIR facture/AVC001-CL0001001/AVC001-CL0001001.pdf
|
||||||
|
----------------------------------------------------------------------------------------------------------------
|
||||||
|
TOTAL 8160.00 8160.00
|
||||||
|
# 5 invoice(s) shown, socid=1
|
||||||
152
.claude/skills/dolibarr-invoice-audit/scripts/audit-invoice.sh
Executable file
152
.claude/skills/dolibarr-invoice-audit/scripts/audit-invoice.sh
Executable file
@@ -0,0 +1,152 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Audit one Arcodange invoice end-to-end.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# audit-invoice.sh <invoice-id>
|
||||||
|
#
|
||||||
|
# JSON side: pulls /invoices/{id} + /thirdparties/{socid}, prints facts
|
||||||
|
# (ref, dates, HT, TVA, TTC, paye flag, mode_reglement, cond_reglement).
|
||||||
|
#
|
||||||
|
# PDF side: pulls /documents/download, base64-decodes, runs pdftotext,
|
||||||
|
# then checks STRUCTURAL PRESENCE of mandatory French invoice mentions.
|
||||||
|
# We do NOT match against static/config/company.json — that file holds
|
||||||
|
# placeholder values today; Dolibarr renders the real legal data from
|
||||||
|
# its own setup. So we check shape, not exact strings.
|
||||||
|
#
|
||||||
|
# Mandatory-mention checklist (codified for SARL / SAS / EURL Arcodange):
|
||||||
|
# - Forme juridique (any of SARL / SAS / EURL / SA / SCI / SASU)
|
||||||
|
# - SIRET (14-digit number, optionally space-separated)
|
||||||
|
# - TVA intracom (FR + 2 chars + 9 digits)
|
||||||
|
# - RCS Évry / RCS Evry / R.C.S. Évry
|
||||||
|
# - NAF-APE code (NNNNL pattern)
|
||||||
|
# - Capital social ("capital" anywhere)
|
||||||
|
# - TVA 259-1° CGI (autoliquidation for non-EU prestations de services)
|
||||||
|
# - L.441-10 — late-payment penalties (BCE+10 or 12,15 %)
|
||||||
|
# - 40 € indemnité forfaitaire (Décret 2012-1115)
|
||||||
|
# - L.123-22 / R.123-237 identification mentions
|
||||||
|
#
|
||||||
|
# Exits 0 if every check passes, 1 otherwise.
|
||||||
|
#
|
||||||
|
# Requires: curl, python3, jq, pdftotext (brew install poppler).
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
DOL_CURL="${SCRIPT_DIR}/../../dolibarr/scripts/dol-curl.sh"
|
||||||
|
|
||||||
|
if [[ $# -lt 1 ]]; then
|
||||||
|
echo "audit-invoice.sh: missing invoice id. Usage: audit-invoice.sh <id>" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
INVOICE_ID="$1"
|
||||||
|
|
||||||
|
command -v pdftotext >/dev/null || { echo "audit-invoice.sh: pdftotext not found (brew install poppler)" >&2; exit 2; }
|
||||||
|
command -v jq >/dev/null || { echo "audit-invoice.sh: jq not found" >&2; exit 2; }
|
||||||
|
|
||||||
|
# --- JSON side -------------------------------------------------------------
|
||||||
|
INV_JSON="$("${DOL_CURL}" "/invoices/${INVOICE_ID}")"
|
||||||
|
SOCID=$( echo "${INV_JSON}" | jq -r .socid)
|
||||||
|
REF=$( echo "${INV_JSON}" | jq -r .ref)
|
||||||
|
DATE_TS=$(echo "${INV_JSON}" | jq -r '.date // 0 | tonumber')
|
||||||
|
DUE_TS=$( echo "${INV_JSON}" | jq -r '.date_lim_reglement // 0 | tonumber')
|
||||||
|
HT=$( echo "${INV_JSON}" | jq -r .total_ht)
|
||||||
|
TVA=$( echo "${INV_JSON}" | jq -r .total_tva)
|
||||||
|
TTC=$( echo "${INV_JSON}" | jq -r .total_ttc)
|
||||||
|
PAYE=$( echo "${INV_JSON}" | jq -r .paye)
|
||||||
|
COND=$( echo "${INV_JSON}" | jq -r '.cond_reglement_code // "-"')
|
||||||
|
PDF_PATH=$(echo "${INV_JSON}" | jq -r .last_main_doc)
|
||||||
|
|
||||||
|
DATE_HUMAN=$(python3 -c "import datetime,sys; ts=int(sys.argv[1]); print(datetime.datetime.fromtimestamp(ts).strftime('%Y-%m-%d') if ts else '-')" "${DATE_TS}")
|
||||||
|
DUE_HUMAN=$( python3 -c "import datetime,sys; ts=int(sys.argv[1]); print(datetime.datetime.fromtimestamp(ts).strftime('%Y-%m-%d') if ts else '-')" "${DUE_TS}")
|
||||||
|
|
||||||
|
TP_JSON="$("${DOL_CURL}" "/thirdparties/${SOCID}")"
|
||||||
|
TP_NAME=$(echo "${TP_JSON}" | jq -r '.name // .ref')
|
||||||
|
|
||||||
|
PAYE_HUMAN=$( [[ "${PAYE}" == "1" ]] && echo "PAID" || echo "UNPAID" )
|
||||||
|
|
||||||
|
# Heuristic: TVA == 0 -> likely reverse-charge (KM is in the US, 259-1° CGI).
|
||||||
|
TVA_NOTE=""
|
||||||
|
if [[ "${TVA}" == "0" || "${TVA}" == "0.00000000" || "${TVA}" == "0.00" ]]; then
|
||||||
|
TVA_NOTE="(TVA=0 → reverse-charge expected; the 259-1° CGI mention MUST be on the PDF)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
================================================================================
|
||||||
|
Invoice ${INVOICE_ID} — ${REF}
|
||||||
|
================================================================================
|
||||||
|
Thirdparty : ${TP_NAME} (socid=${SOCID})
|
||||||
|
Invoice date : ${DATE_HUMAN}
|
||||||
|
Due date : ${DUE_HUMAN} (cond_reglement_code=${COND})
|
||||||
|
Total HT : ${HT}
|
||||||
|
Total TVA : ${TVA} ${TVA_NOTE}
|
||||||
|
Total TTC : ${TTC}
|
||||||
|
Payment state : ${PAYE_HUMAN}
|
||||||
|
PDF : ${PDF_PATH}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [[ -z "${PDF_PATH}" || "${PDF_PATH}" == "null" ]]; then
|
||||||
|
echo
|
||||||
|
echo " /!\\ No PDF attached (last_main_doc is null). Skipping mandatory-mention audit."
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- PDF side --------------------------------------------------------------
|
||||||
|
# original_file must be URL-encoded (the slash between dir and filename too).
|
||||||
|
ENCODED_PDF=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "${PDF_PATH#facture/}")
|
||||||
|
PDF_DOC_JSON="$("${DOL_CURL}" "/documents/download?modulepart=facture&original_file=${ENCODED_PDF}")"
|
||||||
|
PDF_B64="$(echo "${PDF_DOC_JSON}" | jq -r .content)"
|
||||||
|
if [[ -z "${PDF_B64}" || "${PDF_B64}" == "null" ]]; then
|
||||||
|
echo "audit-invoice.sh: no base64 content returned for ${PDF_PATH}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PDF_TMP="$(mktemp -t dolaudit.XXXXXX.pdf)"
|
||||||
|
TXT_TMP="$(mktemp -t dolaudit.XXXXXX.txt)"
|
||||||
|
trap 'rm -f "${PDF_TMP}" "${TXT_TMP}"' EXIT
|
||||||
|
echo "${PDF_B64}" | base64 -d > "${PDF_TMP}"
|
||||||
|
pdftotext -layout "${PDF_TMP}" "${TXT_TMP}"
|
||||||
|
|
||||||
|
pass_count=0
|
||||||
|
fail_count=0
|
||||||
|
check() {
|
||||||
|
# check "label" "extended-regex-pattern"
|
||||||
|
local label="$1" pat="$2"
|
||||||
|
if grep -E -q -- "${pat}" "${TXT_TMP}"; then
|
||||||
|
printf ' [OK] %s\n' "${label}"
|
||||||
|
pass_count=$((pass_count+1))
|
||||||
|
else
|
||||||
|
printf ' [XX] %s (looked for: %s)\n' "${label}" "${pat}"
|
||||||
|
fail_count=$((fail_count+1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo " Mandatory-mention audit — structural presence on the PDF:"
|
||||||
|
check "Forme juridique (SARL/SAS/EURL/SA/SCI/SASU)" "(SARL|SAS|EURL|SCI|SASU|SA[^[:alpha:]])"
|
||||||
|
check "SIRET (14 digits)" "SIRET[[:space:]:]*[0-9]{14}"
|
||||||
|
check "Numéro TVA intracom (FR + 11 chars)" "(TVA|TVA intra)[[:space:]:]*FR[0-9A-Z]{11}"
|
||||||
|
check "RCS / R.C.S. Évry" "(R\\.?C\\.?S\\.?|RCS).*[EÉ]vry"
|
||||||
|
check "NAF-APE code" "(NAF|APE)[[:space:]:-]*[0-9]{4}[A-Z]"
|
||||||
|
check "Capital social" "[Cc]apital"
|
||||||
|
check "TVA 259-1° CGI / autoliquidation" "(259-?1|autoliquidation|reverse[- ]charge)"
|
||||||
|
check "L.441-10 — BCE+10 or 12,15 %" "(L\\.?441-10|BCE[[:space:]]*\\+[[:space:]]*10|12[.,]15[[:space:]]*%)"
|
||||||
|
check "40 € indemnité forfaitaire" "(40[[:space:]]*€|indemnit[eé] forfaitaire|D[eé]cret 2012-1115)"
|
||||||
|
check "L.123-22 / R.123-237" "(L\\.?123-22|R\\.?123-237)"
|
||||||
|
|
||||||
|
# Surface the real legal identifiers extracted from the PDF — useful for
|
||||||
|
# cross-checking against the cohort review without re-running pdftotext.
|
||||||
|
echo
|
||||||
|
echo " Real identifiers extracted from the PDF (informational):"
|
||||||
|
SIRET_FOUND=$( grep -E -o "SIRET[[:space:]:]*[0-9]{14}" "${TXT_TMP}" | head -1 || true)
|
||||||
|
TVA_FOUND=$( grep -E -o "(TVA|TVA intra)[[:space:]:]*FR[0-9A-Z]{11}" "${TXT_TMP}" | head -1 || true)
|
||||||
|
APE_FOUND=$( grep -E -o "(NAF|APE)[[:space:]:-]*[0-9]{4}[A-Z]" "${TXT_TMP}" | head -1 || true)
|
||||||
|
FORM_FOUND=$( grep -E -o "(SARL|SAS|EURL|SCI|SASU|Soci[eé]t[eé] [^[:cntrl:]]{0,40})" "${TXT_TMP}" | head -1 || true)
|
||||||
|
echo " ${FORM_FOUND:-(forme juridique not found)}"
|
||||||
|
echo " ${SIRET_FOUND:-(SIRET not found)}"
|
||||||
|
echo " ${TVA_FOUND:-(TVA intracom not found)}"
|
||||||
|
echo " ${APE_FOUND:-(NAF-APE not found)}"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo " ${pass_count} pass / ${fail_count} fail"
|
||||||
|
[[ ${fail_count} -eq 0 ]]
|
||||||
72
.claude/skills/dolibarr-invoice-audit/scripts/audit-km-thirdparty.sh
Executable file
72
.claude/skills/dolibarr-invoice-audit/scripts/audit-km-thirdparty.sh
Executable file
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Audit the KissMetrics thirdparty record (socid=1) for completeness.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# audit-km-thirdparty.sh
|
||||||
|
#
|
||||||
|
# Checks the contact fields and the idprof1..idprof6 slots where the US EIN
|
||||||
|
# should live for a non-EU customer. Today EIN is missing — this script's
|
||||||
|
# exit-1 IS the finding to surface to the cohort review.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
DOL_CURL="${SCRIPT_DIR}/../../dolibarr/scripts/dol-curl.sh"
|
||||||
|
KM_SOCID=1
|
||||||
|
|
||||||
|
TMP_JSON="$(mktemp -t doltp.XXXXXX.json)"
|
||||||
|
trap 'rm -f "${TMP_JSON}"' EXIT
|
||||||
|
|
||||||
|
"${DOL_CURL}" "/thirdparties/${KM_SOCID}" > "${TMP_JSON}"
|
||||||
|
|
||||||
|
python3 - "${TMP_JSON}" <<'PY'
|
||||||
|
import json, sys
|
||||||
|
with open(sys.argv[1]) as f:
|
||||||
|
d = json.load(f)
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
|
print(f" KissMetrics thirdparty audit — socid={d.get('id')}")
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
required = [
|
||||||
|
("name", d.get("name")),
|
||||||
|
("client flag", "yes" if str(d.get("client")) == "1" else None),
|
||||||
|
("country_code", d.get("country_code")),
|
||||||
|
("state_id", d.get("state_id")),
|
||||||
|
("address", d.get("address")),
|
||||||
|
("zip", d.get("zip")),
|
||||||
|
("town", d.get("town")),
|
||||||
|
("email", d.get("email")),
|
||||||
|
("phone", d.get("phone")),
|
||||||
|
("url", d.get("url")),
|
||||||
|
]
|
||||||
|
idprofs = [(f"idprof{i}", d.get(f"idprof{i}")) for i in range(1, 7)]
|
||||||
|
ein_present = any(v not in (None, "", "0") for _, v in idprofs)
|
||||||
|
|
||||||
|
passes = fails = 0
|
||||||
|
def check(label, value):
|
||||||
|
global passes, fails
|
||||||
|
ok = value not in (None, "", "0")
|
||||||
|
print(f" [{'OK' if ok else 'XX'}] {label:<18} = {value!r}")
|
||||||
|
if ok: passes += 1
|
||||||
|
else: fails += 1
|
||||||
|
|
||||||
|
for label, value in required:
|
||||||
|
check(label, value)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" idprof1..idprof6 (EIN slot for non-EU customers):")
|
||||||
|
for label, value in idprofs:
|
||||||
|
print(f" {label:<10} = {value!r}")
|
||||||
|
print(f" [{'OK' if ein_present else 'XX'}] EIN present (any idprof populated)")
|
||||||
|
if ein_present: passes += 1
|
||||||
|
else: fails += 1
|
||||||
|
|
||||||
|
ao = d.get("array_options") or {}
|
||||||
|
print()
|
||||||
|
print(f" array_options (Dolibarr extrafields): {ao if ao else '(none)'}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(f" {passes} pass / {fails} fail")
|
||||||
|
sys.exit(0 if fails == 0 else 1)
|
||||||
|
PY
|
||||||
73
.claude/skills/dolibarr-invoice-audit/scripts/list-km-invoices.sh
Executable file
73
.claude/skills/dolibarr-invoice-audit/scripts/list-km-invoices.sh
Executable file
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# List Arcodange → KissMetrics invoices with payment state.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# list-km-invoices.sh [--since YYYY-MM-DD]
|
||||||
|
#
|
||||||
|
# KissMetrics is the thirdparty with socid=1 in this Dolibarr. If that ever
|
||||||
|
# changes, update KM_SOCID below or detect it dynamically.
|
||||||
|
#
|
||||||
|
# Credit notes (AVOIRs) are flagged: ref starting with "AVC" or negative HT.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
DOL_CURL="${SCRIPT_DIR}/../../dolibarr/scripts/dol-curl.sh"
|
||||||
|
|
||||||
|
KM_SOCID=1
|
||||||
|
SINCE=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--since) SINCE="$2"; shift 2 ;;
|
||||||
|
-h|--help)
|
||||||
|
sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) echo "list-km-invoices.sh: unknown arg: $1" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
SINCE_EPOCH=0
|
||||||
|
if [[ -n "${SINCE}" ]]; then
|
||||||
|
if SINCE_EPOCH=$(date -j -f "%Y-%m-%d" "${SINCE}" "+%s" 2>/dev/null); then :
|
||||||
|
else SINCE_EPOCH=$(date -d "${SINCE}" "+%s"); fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP_JSON="$(mktemp -t dollist.XXXXXX.json)"
|
||||||
|
trap 'rm -f "${TMP_JSON}"' EXIT
|
||||||
|
|
||||||
|
"${DOL_CURL}" '/invoices?limit=100&sortfield=t.datef&sortorder=DESC' > "${TMP_JSON}"
|
||||||
|
|
||||||
|
python3 - "${TMP_JSON}" "${KM_SOCID}" "${SINCE_EPOCH}" <<'PY'
|
||||||
|
import json, sys, datetime
|
||||||
|
with open(sys.argv[1]) as f:
|
||||||
|
rows = json.load(f)
|
||||||
|
km_socid = sys.argv[2]
|
||||||
|
since_ts = int(sys.argv[3])
|
||||||
|
|
||||||
|
km = [r for r in rows if str(r.get("socid")) == km_socid]
|
||||||
|
km.sort(key=lambda r: int(r.get("date") or 0), reverse=True)
|
||||||
|
|
||||||
|
print(f"{'id':>4} {'ref':<24} {'date':<10} {'HT':>10} {'TVA':>8} {'TTC':>10} {'paid':<5} {'AVC':<5} pdf")
|
||||||
|
print("-" * 112)
|
||||||
|
total_ht = total_ttc = 0.0
|
||||||
|
shown = 0
|
||||||
|
for r in km:
|
||||||
|
ts = int(r.get("date") or 0)
|
||||||
|
if since_ts and ts < since_ts:
|
||||||
|
continue
|
||||||
|
shown += 1
|
||||||
|
dt = datetime.datetime.fromtimestamp(ts).strftime("%Y-%m-%d") if ts else "-"
|
||||||
|
ht = float(r.get("total_ht") or 0)
|
||||||
|
tva = float(r.get("total_tva") or 0)
|
||||||
|
ttc = float(r.get("total_ttc") or 0)
|
||||||
|
paid = "yes" if str(r.get("paye")) == "1" else "no"
|
||||||
|
avc = "AVOIR" if (r.get("ref","").startswith("AVC") or ht < 0) else ""
|
||||||
|
pdf = r.get("last_main_doc") or "-"
|
||||||
|
print(f"{r['id']:>4} {r['ref']:<24} {dt:<10} {ht:>10.2f} {tva:>8.2f} {ttc:>10.2f} {paid:<5} {avc:<5} {pdf}")
|
||||||
|
total_ht += ht
|
||||||
|
total_ttc += ttc
|
||||||
|
print("-" * 112)
|
||||||
|
print(f"{'TOTAL':>40} {total_ht:>10.2f} {' ':>8} {total_ttc:>10.2f}")
|
||||||
|
print(f"# {shown} invoice(s) shown, socid={km_socid}")
|
||||||
|
PY
|
||||||
56
.claude/skills/dolibarr/README.md
Normal file
56
.claude/skills/dolibarr/README.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# dolibarr — one-time setup
|
||||||
|
|
||||||
|
Skill body: [SKILL.md](SKILL.md). This README is the human-facing setup checklist.
|
||||||
|
|
||||||
|
## 1. Create `.env` (mode 600, never committed)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat > .claude/skills/dolibarr/.env <<'EOF'
|
||||||
|
DOLIBARR_URL=https://erp.arcodange.lab
|
||||||
|
DOLIBARR_API_KEY=<get from Dolibarr UI: Users → ai_agent → API key>
|
||||||
|
DOLIBARR_USER=ai_agent
|
||||||
|
DOLIBARR_PASSWORD=<the ai_agent password, only needed for occasional UI login>
|
||||||
|
EOF
|
||||||
|
chmod 600 .claude/skills/dolibarr/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify it's gitignored:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git check-ignore .claude/skills/dolibarr/.env # should print the path
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Grant `ai_agent` the four `voir_tous` permission flags
|
||||||
|
|
||||||
|
`ai_agent` is read-only by design. But Dolibarr's per-record ACL silently filters out invoices and thirdparties unless the `voir_tous` (see-all) flags are ticked. Without them, `/invoices` returns `[]` and `/thirdparties` returns 404 — looks like an empty database.
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
Save. Future modules used by `dolibarr-*` sibling skills (Paiements, Produits, …) need the same treatment.
|
||||||
|
|
||||||
|
## 3. Quick-start test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./.claude/skills/dolibarr/scripts/dol-curl.sh /users/info | jq -r .login
|
||||||
|
# → ai_agent
|
||||||
|
./.claude/skills/dolibarr/scripts/dol-curl.sh /status
|
||||||
|
# → {"success":{"code":200,"dolibarr_version":"22.0.4",...}}
|
||||||
|
./.claude/skills/dolibarr/scripts/dol-curl.sh /thirdparties/1 | jq '{ref, country_code, town}'
|
||||||
|
# → {"ref":"KissMetrics","country_code":"US","town":"St. Petersburg"}
|
||||||
|
```
|
||||||
|
|
||||||
|
If the third one returns HTTP 403 `Access not allowed for login ai_agent on this thirdparty`, the `voir_tous` flags from step 2 are missing.
|
||||||
|
|
||||||
|
## 4. Rotating the API key
|
||||||
|
|
||||||
|
If the key leaks: Dolibarr UI → Users → `ai_agent` → API key → **Generate new** → copy the new value into `.env`. No other change needed; every `dolibarr-*` skill picks it up via `dol-curl.sh`.
|
||||||
|
|
||||||
|
## Pointers
|
||||||
|
|
||||||
|
- Full skill body, endpoint catalogue, gotchas: [SKILL.md](SKILL.md).
|
||||||
|
- First workflow skill that depends on this one: [../dolibarr-invoice-audit/SKILL.md](../dolibarr-invoice-audit/SKILL.md).
|
||||||
139
.claude/skills/dolibarr/SKILL.md
Normal file
139
.claude/skills/dolibarr/SKILL.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|
./.claude/skills/dolibarr/scripts/dol-curl.sh /users/info | jq -r .login
|
||||||
|
# → ai_agent
|
||||||
|
```
|
||||||
|
|
||||||
|
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 /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` | — |
|
||||||
|
| `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 unix epoch (seconds).** `.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
|
||||||
|
```
|
||||||
|
- **`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).
|
||||||
|
- Future workflow skills follow the `dolibarr-<topic>` convention. 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`.
|
||||||
25
.claude/skills/dolibarr/examples/acl_403_thirdparty.json
Normal file
25
.claude/skills/dolibarr/examples/acl_403_thirdparty.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"_captured_when": "2026-05-28, before the four voir_tous permission flags were ticked on ai_agent. Kept here as the reference signature of the ACL trap. Re-occurrence means the permissions regressed.",
|
||||||
|
"_curl": "curl -H 'DOLAPIKEY: <key>' https://erp.arcodange.lab/api/index.php/thirdparties/1",
|
||||||
|
"_http_status": 403,
|
||||||
|
"error": {
|
||||||
|
"code": 403,
|
||||||
|
"message": "Forbidden: Access not allowed for login ai_agent on this thirdparty"
|
||||||
|
},
|
||||||
|
"debug": {
|
||||||
|
"source": "api_thirdparties.class.php:2403 at call stage",
|
||||||
|
"stages": {
|
||||||
|
"success": [
|
||||||
|
"get",
|
||||||
|
"route",
|
||||||
|
"negotiate",
|
||||||
|
"authenticate",
|
||||||
|
"validate"
|
||||||
|
],
|
||||||
|
"failure": [
|
||||||
|
"call",
|
||||||
|
"message"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"filename": "FAC002-CL0001002.pdf",
|
||||||
|
"content-type": "application/pdf",
|
||||||
|
"filesize": 62345,
|
||||||
|
"content": "JVBERi0xLjcKJeLjz9MKNiAwIG9iago8PCAvVHlwZSAvUGFnZSAvUGFyZW50IDEgMCBSIC9MYXN0TW9kaWZpZWQgKEQ6MjAyNjAzMTMxNTIxNTcrMDEnMDAn...[truncated for repo; 62345-byte PDF]",
|
||||||
|
"encoding": "base64"
|
||||||
|
}
|
||||||
273
.claude/skills/dolibarr/examples/invoice_detail.json
Normal file
273
.claude/skills/dolibarr/examples/invoice_detail.json
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
{
|
||||||
|
"module": null,
|
||||||
|
"id": "12",
|
||||||
|
"entity": "1",
|
||||||
|
"import_key": null,
|
||||||
|
"array_options": [],
|
||||||
|
"array_languages": null,
|
||||||
|
"contacts_ids": [],
|
||||||
|
"contacts_ids_internal": null,
|
||||||
|
"linkedObjectsIds": [],
|
||||||
|
"fk_project": null,
|
||||||
|
"contact_id": null,
|
||||||
|
"user": null,
|
||||||
|
"origin_type": null,
|
||||||
|
"origin_id": null,
|
||||||
|
"ref": "FAC002-CL0001002",
|
||||||
|
"ref_ext": null,
|
||||||
|
"statut": "2",
|
||||||
|
"status": "2",
|
||||||
|
"country_id": null,
|
||||||
|
"country_code": null,
|
||||||
|
"state_id": null,
|
||||||
|
"region_id": null,
|
||||||
|
"mode_reglement_id": "2",
|
||||||
|
"cond_reglement_id": "10",
|
||||||
|
"demand_reason_id": null,
|
||||||
|
"transport_mode_id": null,
|
||||||
|
"shipping_method_id": null,
|
||||||
|
"shipping_method": null,
|
||||||
|
"fk_multicurrency": "0",
|
||||||
|
"multicurrency_code": "EUR",
|
||||||
|
"multicurrency_tx": "1.00000000",
|
||||||
|
"multicurrency_total_ht": "5100.00000000",
|
||||||
|
"multicurrency_total_tva": "0.00000000",
|
||||||
|
"multicurrency_total_localtax1": null,
|
||||||
|
"multicurrency_total_localtax2": null,
|
||||||
|
"multicurrency_total_ttc": "5100.00000000",
|
||||||
|
"last_main_doc": "facture/FAC002-CL0001002/FAC002-CL0001002.pdf",
|
||||||
|
"fk_account": "2",
|
||||||
|
"note_public": null,
|
||||||
|
"note_private": "Généré depuis la facture modèle récurrente Kiss Metrics Invoice",
|
||||||
|
"total_ht": "5100.00000000",
|
||||||
|
"total_tva": "0.00000000",
|
||||||
|
"total_localtax1": "0.00000000",
|
||||||
|
"total_localtax2": "0.00000000",
|
||||||
|
"total_ttc": "5100.00000000",
|
||||||
|
"lines": [
|
||||||
|
{
|
||||||
|
"module": null,
|
||||||
|
"id": "12",
|
||||||
|
"entity": null,
|
||||||
|
"import_key": null,
|
||||||
|
"array_options": [],
|
||||||
|
"array_languages": null,
|
||||||
|
"contacts_ids": null,
|
||||||
|
"contacts_ids_internal": null,
|
||||||
|
"linkedObjectsIds": null,
|
||||||
|
"origin_type": null,
|
||||||
|
"origin_id": null,
|
||||||
|
"ref": "KM-cloud-devops",
|
||||||
|
"ref_ext": "",
|
||||||
|
"statut": null,
|
||||||
|
"status": null,
|
||||||
|
"state_id": null,
|
||||||
|
"region_id": null,
|
||||||
|
"demand_reason_id": null,
|
||||||
|
"transport_mode_id": null,
|
||||||
|
"shipping_method": null,
|
||||||
|
"multicurrency_tx": null,
|
||||||
|
"multicurrency_total_ht": "5100.00000000",
|
||||||
|
"multicurrency_total_tva": "0.00000000",
|
||||||
|
"multicurrency_total_localtax1": null,
|
||||||
|
"multicurrency_total_localtax2": null,
|
||||||
|
"multicurrency_total_ttc": "5100.00000000",
|
||||||
|
"last_main_doc": null,
|
||||||
|
"fk_account": null,
|
||||||
|
"total_ht": "5100.00000000",
|
||||||
|
"total_tva": "0.00000000",
|
||||||
|
"total_localtax1": "0.00000000",
|
||||||
|
"total_localtax2": "0.00000000",
|
||||||
|
"total_ttc": "5100.00000000",
|
||||||
|
"lines": null,
|
||||||
|
"actiontypecode": null,
|
||||||
|
"civility_code": null,
|
||||||
|
"date_creation": null,
|
||||||
|
"date_validation": null,
|
||||||
|
"date_modification": null,
|
||||||
|
"tms": null,
|
||||||
|
"date_cloture": null,
|
||||||
|
"user_author": null,
|
||||||
|
"user_creation": null,
|
||||||
|
"user_creation_id": null,
|
||||||
|
"user_valid": null,
|
||||||
|
"user_validation": null,
|
||||||
|
"user_validation_id": null,
|
||||||
|
"user_closing_id": null,
|
||||||
|
"user_modification": null,
|
||||||
|
"user_modification_id": null,
|
||||||
|
"fk_user_creat": null,
|
||||||
|
"fk_user_modif": null,
|
||||||
|
"specimen": 0,
|
||||||
|
"totalpaid": null,
|
||||||
|
"extraparams": [],
|
||||||
|
"product": null,
|
||||||
|
"cond_reglement_supplier_id": null,
|
||||||
|
"deposit_percent": null,
|
||||||
|
"retained_warranty_fk_cond_reglement": null,
|
||||||
|
"warehouse_id": null,
|
||||||
|
"parent_element": "",
|
||||||
|
"fk_parent_attribute": "",
|
||||||
|
"rowid": "12",
|
||||||
|
"fk_unit": null,
|
||||||
|
"date_debut_prevue": null,
|
||||||
|
"date_debut_reel": null,
|
||||||
|
"date_fin_prevue": null,
|
||||||
|
"date_fin_reel": null,
|
||||||
|
"weight": null,
|
||||||
|
"weight_units": null,
|
||||||
|
"length": null,
|
||||||
|
"length_units": null,
|
||||||
|
"width": null,
|
||||||
|
"width_units": null,
|
||||||
|
"height": null,
|
||||||
|
"height_units": null,
|
||||||
|
"surface": null,
|
||||||
|
"surface_units": null,
|
||||||
|
"volume": null,
|
||||||
|
"volume_units": null,
|
||||||
|
"multilangs": null,
|
||||||
|
"product_type": "1",
|
||||||
|
"fk_product": "2",
|
||||||
|
"desc": "TVA non applicable – Article 259-1 du CGI – Prestation de services localisée hors de France (USA)",
|
||||||
|
"description": "TVA non applicable – Article 259-1 du CGI – Prestation de services localisée hors de France (USA)",
|
||||||
|
"product_ref": "KM-cloud-devops",
|
||||||
|
"product_label": "KissMetrics - Cloud Devops - 1 day",
|
||||||
|
"product_barcode": null,
|
||||||
|
"product_desc": "",
|
||||||
|
"fk_product_type": "1",
|
||||||
|
"qty": "10",
|
||||||
|
"duree": null,
|
||||||
|
"remise_percent": "0",
|
||||||
|
"info_bits": "0",
|
||||||
|
"special_code": "0",
|
||||||
|
"subprice": "510.00000000",
|
||||||
|
"subprice_ttc": null,
|
||||||
|
"tva_tx": "0.0000",
|
||||||
|
"multicurrency_subprice": "510.00000000",
|
||||||
|
"multicurrency_subprice_ttc": null,
|
||||||
|
"label": null,
|
||||||
|
"libelle": "KissMetrics - Cloud Devops - 1 day",
|
||||||
|
"price": null,
|
||||||
|
"vat_src_code": "",
|
||||||
|
"localtax1_tx": "0.0000",
|
||||||
|
"localtax2_tx": "0.0000",
|
||||||
|
"localtax1_type": "0",
|
||||||
|
"localtax2_type": "0",
|
||||||
|
"remise": null,
|
||||||
|
"revenuestamp": null,
|
||||||
|
"date_start_fill": null,
|
||||||
|
"date_end_fill": null,
|
||||||
|
"buy_price_ht": null,
|
||||||
|
"buyprice": null,
|
||||||
|
"pa_ht": "0.00000000",
|
||||||
|
"marge_tx": "",
|
||||||
|
"marque_tx": "100",
|
||||||
|
"fk_user_author": null,
|
||||||
|
"fk_accounting_account": "7",
|
||||||
|
"fk_facture": "12",
|
||||||
|
"fk_parent_line": null,
|
||||||
|
"fk_remise_except": null,
|
||||||
|
"rang": "1",
|
||||||
|
"fk_fournprice": null,
|
||||||
|
"tva_npr": null,
|
||||||
|
"batch": "",
|
||||||
|
"fk_warehouse": "0",
|
||||||
|
"fk_code_ventilation": 0,
|
||||||
|
"date_start": 1769986800,
|
||||||
|
"date_end": 1770937200,
|
||||||
|
"situation_percent": "100",
|
||||||
|
"fk_prev_id": null,
|
||||||
|
"packaging": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"actiontypecode": null,
|
||||||
|
"name": null,
|
||||||
|
"lastname": null,
|
||||||
|
"firstname": null,
|
||||||
|
"civility_id": null,
|
||||||
|
"civility_code": null,
|
||||||
|
"date_creation": 1771929906,
|
||||||
|
"date_validation": 1771887600,
|
||||||
|
"date_modification": 1773408117,
|
||||||
|
"tms": null,
|
||||||
|
"date_cloture": null,
|
||||||
|
"user_author": null,
|
||||||
|
"user_creation": null,
|
||||||
|
"user_creation_id": "2",
|
||||||
|
"user_valid": null,
|
||||||
|
"user_validation": null,
|
||||||
|
"user_validation_id": "2",
|
||||||
|
"user_closing_id": null,
|
||||||
|
"user_modification": null,
|
||||||
|
"user_modification_id": null,
|
||||||
|
"fk_user_creat": null,
|
||||||
|
"fk_user_modif": null,
|
||||||
|
"specimen": 0,
|
||||||
|
"totalpaid": 5100,
|
||||||
|
"extraparams": [],
|
||||||
|
"product": null,
|
||||||
|
"cond_reglement_supplier_id": null,
|
||||||
|
"deposit_percent": null,
|
||||||
|
"retained_warranty_fk_cond_reglement": "0",
|
||||||
|
"warehouse_id": null,
|
||||||
|
"title": null,
|
||||||
|
"type": "0",
|
||||||
|
"subtype": null,
|
||||||
|
"fk_soc": null,
|
||||||
|
"socid": "1",
|
||||||
|
"paye": "1",
|
||||||
|
"date": 1771887600,
|
||||||
|
"date_lim_reglement": 1774908000,
|
||||||
|
"cond_reglement_code": "10DENDMONTH",
|
||||||
|
"cond_reglement_label": null,
|
||||||
|
"cond_reglement_doc": "Due in 10 days, end of month",
|
||||||
|
"mode_reglement_code": "VIR",
|
||||||
|
"revenuestamp": "0.00000000",
|
||||||
|
"totaldeposits": null,
|
||||||
|
"totalcreditnotes": null,
|
||||||
|
"sumpayed": "5100.00000000",
|
||||||
|
"sumpayed_multicurrency": null,
|
||||||
|
"sumdeposit": null,
|
||||||
|
"sumdeposit_multicurrency": null,
|
||||||
|
"sumcreditnote": null,
|
||||||
|
"sumcreditnote_multicurrency": null,
|
||||||
|
"remaintopay": "0",
|
||||||
|
"nbofopendirectdebitorcredittransfer": null,
|
||||||
|
"creditnote_ids": [],
|
||||||
|
"stripechargedone": null,
|
||||||
|
"stripechargeerror": null,
|
||||||
|
"description": null,
|
||||||
|
"ref_client": null,
|
||||||
|
"situation_cycle_ref": null,
|
||||||
|
"close_code": null,
|
||||||
|
"close_note": null,
|
||||||
|
"postactionmessages": null,
|
||||||
|
"fk_incoterms": "0",
|
||||||
|
"label_incoterms": null,
|
||||||
|
"location_incoterms": "",
|
||||||
|
"fk_user_author": "2",
|
||||||
|
"fk_user_valid": "2",
|
||||||
|
"datem": 1773408117,
|
||||||
|
"delivery_date": null,
|
||||||
|
"ref_customer": null,
|
||||||
|
"resteapayer": null,
|
||||||
|
"module_source": null,
|
||||||
|
"pos_source": null,
|
||||||
|
"fk_fac_rec_source": "1",
|
||||||
|
"fk_facture_source": null,
|
||||||
|
"line": null,
|
||||||
|
"fac_rec": null,
|
||||||
|
"date_pointoftax": "",
|
||||||
|
"situation_counter": null,
|
||||||
|
"situation_final": "0",
|
||||||
|
"tab_previous_situation_invoice": [],
|
||||||
|
"tab_next_situation_invoice": [],
|
||||||
|
"retained_warranty": "0",
|
||||||
|
"retained_warranty_date_limit": "",
|
||||||
|
"availability_id": null,
|
||||||
|
"date_closing": null,
|
||||||
|
"source": null,
|
||||||
|
"remise_percent": null,
|
||||||
|
"online_payment_url": "https://erp.arcodange.lab/public/payment/newpayment.php?source=invoice&ref=FAC002-CL0001002"
|
||||||
|
}
|
||||||
1369
.claude/skills/dolibarr/examples/invoices_list.json
Normal file
1369
.claude/skills/dolibarr/examples/invoices_list.json
Normal file
File diff suppressed because it is too large
Load Diff
7
.claude/skills/dolibarr/examples/status.json
Normal file
7
.claude/skills/dolibarr/examples/status.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"success": {
|
||||||
|
"code": 200,
|
||||||
|
"dolibarr_version": "22.0.4",
|
||||||
|
"access_locked": "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
167
.claude/skills/dolibarr/examples/thirdparty_km.json
Normal file
167
.claude/skills/dolibarr/examples/thirdparty_km.json
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
{
|
||||||
|
"module": "societe",
|
||||||
|
"id": "1",
|
||||||
|
"entity": "1",
|
||||||
|
"import_key": null,
|
||||||
|
"array_options": [],
|
||||||
|
"array_languages": null,
|
||||||
|
"contacts_ids": null,
|
||||||
|
"contacts_ids_internal": null,
|
||||||
|
"linkedObjectsIds": null,
|
||||||
|
"canvas": null,
|
||||||
|
"fk_project": null,
|
||||||
|
"contact_id": null,
|
||||||
|
"user": null,
|
||||||
|
"origin_type": null,
|
||||||
|
"origin_id": null,
|
||||||
|
"ref": "KissMetrics",
|
||||||
|
"ref_ext": null,
|
||||||
|
"statut": null,
|
||||||
|
"status": "1",
|
||||||
|
"country_id": "11",
|
||||||
|
"country_code": "US",
|
||||||
|
"state_id": "1167",
|
||||||
|
"region_id": "297",
|
||||||
|
"barcode_type": null,
|
||||||
|
"barcode_type_coder": null,
|
||||||
|
"mode_reglement_id": null,
|
||||||
|
"cond_reglement_id": null,
|
||||||
|
"demand_reason_id": null,
|
||||||
|
"transport_mode_id": null,
|
||||||
|
"shipping_method_id": null,
|
||||||
|
"shipping_method": null,
|
||||||
|
"fk_multicurrency": "0",
|
||||||
|
"multicurrency_code": "",
|
||||||
|
"multicurrency_tx": null,
|
||||||
|
"multicurrency_total_ht": null,
|
||||||
|
"multicurrency_total_tva": null,
|
||||||
|
"multicurrency_total_localtax1": null,
|
||||||
|
"multicurrency_total_localtax2": null,
|
||||||
|
"multicurrency_total_ttc": null,
|
||||||
|
"last_main_doc": null,
|
||||||
|
"fk_account": "0",
|
||||||
|
"note_public": null,
|
||||||
|
"note_private": null,
|
||||||
|
"actiontypecode": null,
|
||||||
|
"name": "KissMetrics",
|
||||||
|
"lastname": null,
|
||||||
|
"firstname": null,
|
||||||
|
"civility_id": null,
|
||||||
|
"civility_code": null,
|
||||||
|
"date_creation": 1769898064,
|
||||||
|
"date_validation": null,
|
||||||
|
"date_modification": 1771926531,
|
||||||
|
"tms": null,
|
||||||
|
"date_cloture": null,
|
||||||
|
"user_author": null,
|
||||||
|
"user_creation": null,
|
||||||
|
"user_creation_id": "1",
|
||||||
|
"user_valid": null,
|
||||||
|
"user_validation": null,
|
||||||
|
"user_validation_id": null,
|
||||||
|
"user_closing_id": null,
|
||||||
|
"user_modification": null,
|
||||||
|
"user_modification_id": "1",
|
||||||
|
"fk_user_creat": null,
|
||||||
|
"fk_user_modif": null,
|
||||||
|
"specimen": 0,
|
||||||
|
"totalpaid": null,
|
||||||
|
"extraparams": [],
|
||||||
|
"product": null,
|
||||||
|
"cond_reglement_supplier_id": null,
|
||||||
|
"deposit_percent": null,
|
||||||
|
"retained_warranty_fk_cond_reglement": null,
|
||||||
|
"warehouse_id": null,
|
||||||
|
"SupplierCategories": [],
|
||||||
|
"prefixCustomerIsRequired": null,
|
||||||
|
"name_alias": "",
|
||||||
|
"phone": null,
|
||||||
|
"phone_mobile": null,
|
||||||
|
"fax": null,
|
||||||
|
"email": "evan@kissmetrics.io",
|
||||||
|
"no_email": null,
|
||||||
|
"skype": null,
|
||||||
|
"twitter": null,
|
||||||
|
"facebook": null,
|
||||||
|
"linkedin": null,
|
||||||
|
"url": null,
|
||||||
|
"barcode": null,
|
||||||
|
"idprof1": "",
|
||||||
|
"idprof2": "",
|
||||||
|
"idprof3": "",
|
||||||
|
"idprof4": "",
|
||||||
|
"idprof5": "",
|
||||||
|
"idprof6": "",
|
||||||
|
"idprof7": null,
|
||||||
|
"idprof8": null,
|
||||||
|
"idprof9": null,
|
||||||
|
"idprof10": null,
|
||||||
|
"socialobject": null,
|
||||||
|
"tva_assuj": "0",
|
||||||
|
"tva_intra": "",
|
||||||
|
"vat_reverse_charge": 0,
|
||||||
|
"localtax1_assuj": null,
|
||||||
|
"localtax1_value": "0.0000",
|
||||||
|
"localtax2_assuj": null,
|
||||||
|
"localtax2_value": "0.0000",
|
||||||
|
"managers": null,
|
||||||
|
"capital": null,
|
||||||
|
"typent_id": "3",
|
||||||
|
"typent_code": "TE_MEDIUM",
|
||||||
|
"effectif": "1 - 5",
|
||||||
|
"effectif_id": "1",
|
||||||
|
"forme_juridique_code": null,
|
||||||
|
"forme_juridique": "",
|
||||||
|
"remise_percent": 0,
|
||||||
|
"remise_supplier_percent": "0",
|
||||||
|
"mode_reglement_supplier_id": null,
|
||||||
|
"transport_mode_supplier_id": null,
|
||||||
|
"fk_prospectlevel": "",
|
||||||
|
"client": "1",
|
||||||
|
"prospect": 0,
|
||||||
|
"fournisseur": "0",
|
||||||
|
"code_client": "CL0001",
|
||||||
|
"code_fournisseur": null,
|
||||||
|
"code_compta_client": "411KISSME",
|
||||||
|
"accountancy_code_customer_general": null,
|
||||||
|
"accountancy_code_customer": null,
|
||||||
|
"code_compta_fournisseur": null,
|
||||||
|
"accountancy_code_supplier_general": null,
|
||||||
|
"accountancy_code_supplier": null,
|
||||||
|
"code_compta_product": null,
|
||||||
|
"stcomm_id": "0",
|
||||||
|
"stcomm_picto": null,
|
||||||
|
"status_prospect_label": "Never contacted",
|
||||||
|
"price_level": null,
|
||||||
|
"outstanding_limit": null,
|
||||||
|
"order_min_amount": null,
|
||||||
|
"supplier_order_min_amount": null,
|
||||||
|
"parent": null,
|
||||||
|
"default_lang": null,
|
||||||
|
"ip": null,
|
||||||
|
"webservices_url": null,
|
||||||
|
"webservices_key": null,
|
||||||
|
"logo": null,
|
||||||
|
"logo_small": null,
|
||||||
|
"logo_mini": null,
|
||||||
|
"logo_squarred": null,
|
||||||
|
"logo_squarred_small": null,
|
||||||
|
"logo_squarred_mini": null,
|
||||||
|
"accountancy_code_sell": "",
|
||||||
|
"accountancy_code_buy": "",
|
||||||
|
"currency_code": null,
|
||||||
|
"fk_warehouse": null,
|
||||||
|
"termsofsale": null,
|
||||||
|
"partnerships": [],
|
||||||
|
"bank_account": null,
|
||||||
|
"code_compta": null,
|
||||||
|
"fk_incoterms": "0",
|
||||||
|
"label_incoterms": null,
|
||||||
|
"location_incoterms": null,
|
||||||
|
"socialnetworks": [],
|
||||||
|
"address": "2850 34th Street North, 307",
|
||||||
|
"zip": "33713",
|
||||||
|
"town": "St. Petersburg",
|
||||||
|
"absolute_discount": "0",
|
||||||
|
"absolute_creditnote": "0"
|
||||||
|
}
|
||||||
146
.claude/skills/dolibarr/examples/users_info.json
Normal file
146
.claude/skills/dolibarr/examples/users_info.json
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
{
|
||||||
|
"module": null,
|
||||||
|
"id": "3",
|
||||||
|
"entity": "1",
|
||||||
|
"import_key": null,
|
||||||
|
"array_options": [],
|
||||||
|
"array_languages": null,
|
||||||
|
"contacts_ids": null,
|
||||||
|
"contacts_ids_internal": null,
|
||||||
|
"linkedObjectsIds": null,
|
||||||
|
"canvas": null,
|
||||||
|
"fk_project": null,
|
||||||
|
"contact_id": null,
|
||||||
|
"user": null,
|
||||||
|
"origin_type": null,
|
||||||
|
"origin_id": null,
|
||||||
|
"ref": "3",
|
||||||
|
"ref_ext": null,
|
||||||
|
"statut": "1",
|
||||||
|
"status": "1",
|
||||||
|
"country_id": null,
|
||||||
|
"country_code": "",
|
||||||
|
"state_id": null,
|
||||||
|
"region_id": null,
|
||||||
|
"barcode_type": null,
|
||||||
|
"barcode_type_coder": null,
|
||||||
|
"mode_reglement_id": null,
|
||||||
|
"cond_reglement_id": null,
|
||||||
|
"demand_reason_id": null,
|
||||||
|
"transport_mode_id": null,
|
||||||
|
"shipping_method": null,
|
||||||
|
"fk_multicurrency": null,
|
||||||
|
"multicurrency_code": null,
|
||||||
|
"multicurrency_tx": null,
|
||||||
|
"multicurrency_total_ht": null,
|
||||||
|
"multicurrency_total_tva": null,
|
||||||
|
"multicurrency_total_localtax1": null,
|
||||||
|
"multicurrency_total_localtax2": null,
|
||||||
|
"multicurrency_total_ttc": null,
|
||||||
|
"last_main_doc": null,
|
||||||
|
"fk_account": null,
|
||||||
|
"note_public": "",
|
||||||
|
"note_private": "",
|
||||||
|
"actiontypecode": null,
|
||||||
|
"name": null,
|
||||||
|
"lastname": "AI Agent",
|
||||||
|
"firstname": "",
|
||||||
|
"civility_id": null,
|
||||||
|
"civility_code": "",
|
||||||
|
"date_creation": null,
|
||||||
|
"date_validation": null,
|
||||||
|
"date_modification": null,
|
||||||
|
"tms": null,
|
||||||
|
"date_cloture": null,
|
||||||
|
"user_author": null,
|
||||||
|
"user_creation": null,
|
||||||
|
"user_creation_id": null,
|
||||||
|
"user_valid": null,
|
||||||
|
"user_validation": null,
|
||||||
|
"user_validation_id": null,
|
||||||
|
"user_closing_id": null,
|
||||||
|
"user_modification": null,
|
||||||
|
"user_modification_id": null,
|
||||||
|
"fk_user_creat": null,
|
||||||
|
"fk_user_modif": null,
|
||||||
|
"specimen": 0,
|
||||||
|
"totalpaid": null,
|
||||||
|
"extraparams": [],
|
||||||
|
"product": null,
|
||||||
|
"cond_reglement_supplier_id": null,
|
||||||
|
"deposit_percent": null,
|
||||||
|
"retained_warranty_fk_cond_reglement": null,
|
||||||
|
"warehouse_id": null,
|
||||||
|
"employee": "1",
|
||||||
|
"fullname": null,
|
||||||
|
"gender": null,
|
||||||
|
"birth": "",
|
||||||
|
"email": "",
|
||||||
|
"email_oauth2": null,
|
||||||
|
"personal_email": "",
|
||||||
|
"socialnetworks": [],
|
||||||
|
"job": "AI Agent",
|
||||||
|
"signature": "Claude AI",
|
||||||
|
"office_phone": "",
|
||||||
|
"office_fax": "",
|
||||||
|
"user_mobile": "",
|
||||||
|
"personal_mobile": "",
|
||||||
|
"admin": "0",
|
||||||
|
"login": "ai_agent",
|
||||||
|
"pass_crypted": null,
|
||||||
|
"datec": 1779984197,
|
||||||
|
"datem": 1779977142,
|
||||||
|
"socid": null,
|
||||||
|
"fk_member": null,
|
||||||
|
"fk_user": null,
|
||||||
|
"fk_user_expense_validator": null,
|
||||||
|
"fk_user_holiday_validator": null,
|
||||||
|
"clicktodial_url": null,
|
||||||
|
"clicktodial_login": null,
|
||||||
|
"clicktodial_poste": null,
|
||||||
|
"datelastpassvalidation": "2026-05-28 18:05:42",
|
||||||
|
"datelastlogin": "",
|
||||||
|
"datepreviouslogin": "",
|
||||||
|
"flagdelsessionsbefore": 1779984337,
|
||||||
|
"iplastlogin": null,
|
||||||
|
"ippreviouslogin": null,
|
||||||
|
"datestartvalidity": "",
|
||||||
|
"dateendvalidity": "",
|
||||||
|
"photo": null,
|
||||||
|
"lang": null,
|
||||||
|
"rights": {
|
||||||
|
"user": {
|
||||||
|
"user": {},
|
||||||
|
"self": {},
|
||||||
|
"user_advance": {},
|
||||||
|
"self_advance": {},
|
||||||
|
"group_advance": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"user_group_list": [],
|
||||||
|
"conf": {},
|
||||||
|
"users": [],
|
||||||
|
"parentof": null,
|
||||||
|
"accountancy_code_user_general": "",
|
||||||
|
"accountancy_code": "",
|
||||||
|
"thm": null,
|
||||||
|
"tjm": null,
|
||||||
|
"salary": null,
|
||||||
|
"salaryextra": null,
|
||||||
|
"weeklyhours": null,
|
||||||
|
"color": "ff7f00",
|
||||||
|
"dateemployment": "",
|
||||||
|
"dateemploymentend": "",
|
||||||
|
"default_c_exp_tax_cat": null,
|
||||||
|
"ref_employee": "",
|
||||||
|
"national_registration_number": "",
|
||||||
|
"default_range": null,
|
||||||
|
"fk_warehouse": null,
|
||||||
|
"fk_establishment": null,
|
||||||
|
"label_establishment": null,
|
||||||
|
"usergroup_entity": null,
|
||||||
|
"address": "",
|
||||||
|
"zip": "",
|
||||||
|
"town": "",
|
||||||
|
"url": null
|
||||||
|
}
|
||||||
61
.claude/skills/dolibarr/scripts/dol-curl.sh
Executable file
61
.claude/skills/dolibarr/scripts/dol-curl.sh
Executable file
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Tiny read-only curl wrapper for the Arcodange Dolibarr API.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# dol-curl.sh <path-with-query> # e.g. dol-curl.sh /invoices?limit=5
|
||||||
|
# dol-curl.sh -i <path> # include response headers
|
||||||
|
# dol-curl.sh -o file.json <path> # write body to file
|
||||||
|
#
|
||||||
|
# Reads DOLIBARR_URL and DOLIBARR_API_KEY from the sibling .env file
|
||||||
|
# (.claude/skills/dolibarr/.env), mode 600, gitignored.
|
||||||
|
# Exits non-zero on HTTP >=400 and prints the response body on stderr.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ENV_FILE="${SCRIPT_DIR}/../.env"
|
||||||
|
|
||||||
|
if [[ ! -f "${ENV_FILE}" ]]; then
|
||||||
|
echo "dol-curl.sh: missing ${ENV_FILE}" >&2
|
||||||
|
echo " Create it with DOLIBARR_URL and DOLIBARR_API_KEY. See dolibarr/README.md." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
set -a; source "${ENV_FILE}"; set +a
|
||||||
|
|
||||||
|
: "${DOLIBARR_URL:?dol-curl.sh: DOLIBARR_URL not set in .env}"
|
||||||
|
: "${DOLIBARR_API_KEY:?dol-curl.sh: DOLIBARR_API_KEY not set in .env}"
|
||||||
|
|
||||||
|
# Last positional arg is the API path; everything before it is passed through to curl.
|
||||||
|
if [[ $# -lt 1 ]]; then
|
||||||
|
echo "dol-curl.sh: missing API path. Example: dol-curl.sh /users/info" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
PASSTHRU=()
|
||||||
|
while [[ $# -gt 1 ]]; do
|
||||||
|
PASSTHRU+=("$1")
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
API_PATH="$1"
|
||||||
|
|
||||||
|
# Two-stage call: capture HTTP code + body separately so we can fail clearly
|
||||||
|
# while still letting the user pipe stdout into jq.
|
||||||
|
BODY_FILE="$(mktemp -t dolcurl.XXXXXX)"
|
||||||
|
trap 'rm -f "${BODY_FILE}"' EXIT
|
||||||
|
|
||||||
|
HTTP_CODE=$(curl -sS \
|
||||||
|
-H "DOLAPIKEY: ${DOLIBARR_API_KEY}" \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
--max-time 30 \
|
||||||
|
-o "${BODY_FILE}" \
|
||||||
|
-w "%{http_code}" \
|
||||||
|
${PASSTHRU[@]+"${PASSTHRU[@]}"} \
|
||||||
|
"${DOLIBARR_URL}/api/index.php${API_PATH}")
|
||||||
|
|
||||||
|
cat "${BODY_FILE}"
|
||||||
|
if [[ "${HTTP_CODE}" -ge 400 ]]; then
|
||||||
|
echo "dol-curl.sh: HTTP ${HTTP_CODE} on ${API_PATH}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -2,3 +2,10 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode
|
.vscode
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# Secrets — defense-in-depth (the .env rule above already matches nested .env files)
|
||||||
|
*.credentials
|
||||||
|
secrets/
|
||||||
|
*.key
|
||||||
|
.claude/skills/**/.env
|
||||||
|
.claude/skills/**/examples/document_*.bin
|
||||||
Reference in New Issue
Block a user