add dolibarr api skills for read-only inspection
First two of an expected family of dolibarr-* skills: - dolibarr/: platform reference — DOLAPIKEY auth, the voir_tous ACL trap, endpoint catalogue, the dol-curl.sh wrapper, .env credentials layout (gitignored, mode 600). Every future workflow skill depends on this one. - dolibarr-invoice-audit/: first workflow — list KissMetrics invoices, audit one invoice end-to-end (JSON facts + PDF mandatory-mention checklist against the French legal corpus), audit the KissMetrics thirdparty record. Live captures in examples/ include real audit findings to surface to the Arcodange × KissMetrics cohort review: PDFs are missing capital social, L.441-10 penalties, 40 € indemnity, L.123-22 / R.123-237; KissMetrics thirdparty has no EIN (idprof1..6 all empty); static/config/company.json holds placeholder values and a wrong forme juridique (claims SAS, the real Dolibarr is SARL). .gitignore hardened with *.credentials, secrets/, *.key, and an explicit .claude/skills/**/.env pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user