From e748efd8f08e023039ce41ae1ba3564cd612cc17 Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Tue, 30 Jun 2026 00:17:41 +0200 Subject: [PATCH] =?UTF-8?q?feat(payment):=20return=20the=20bank=20transact?= =?UTF-8?q?ion=20id=20on=20r=C3=A8glements=20(reconciliation=20link)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A payment only returned its paiement id, which isn't what bank reconciliation keys on. payment-record.sh now emits {id, bank_transaction_id, num}: - bank_transaction_id = the Dolibarr bank line (llx_bank.fk_bank_line) the payment created, resolved via GET /{invoices|supplierinvoices}/{id}/payments (correlated by num, else the most recent line). Works for customer and supplier. - num stores the originating bank tx id (Qonto/Wise) and lands on that bank line's num_chq — so arcodange-bank-reco can match a règlement to a statement line by id instead of fuzzy amount/date. Both ends captured at write time. Proven live: customer {id:13,bank_transaction_id:35,num:QONTO-TX-1234}, supplier {id:16,bank_transaction_id:36,num:WISE-TX-5678}; llx_bank rows 35/36 carry the refs in num_chq. promote-apply still extracts .id unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../skills/dolibarr-sandbox-write/SKILL.md | 12 +++-- .../scripts/payment-record.sh | 48 ++++++++++++++++--- bin/arcodange | 2 +- 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/.claude/skills/dolibarr-sandbox-write/SKILL.md b/.claude/skills/dolibarr-sandbox-write/SKILL.md index 83e83ea..6a4be77 100644 --- a/.claude/skills/dolibarr-sandbox-write/SKILL.md +++ b/.claude/skills/dolibarr-sandbox-write/SKILL.md @@ -96,15 +96,21 @@ to leave a draft. Emits `{id, ref, ref_supplier, total_ht, total_ttc, statut}`. ### 3 · Payment (règlement) — `scripts/payment-record.sh` ```sh -echo '{"invoice_id":19,"mode":"VIR","account_id":1}' | scripts/payment-record.sh -echo '{"invoice_id":13,"kind":"supplier","mode":"VIR","account_id":1,"amount":96}' \ +echo '{"invoice_id":19,"mode":"VIR","account_id":1,"num":"QONTO-TX-1234"}' | scripts/payment-record.sh +echo '{"invoice_id":13,"kind":"supplier","mode":"VIR","account_id":1,"amount":96,"num":"WISE-TX-5678"}' \ | scripts/payment-record.sh ``` The invoice must be **validated** first. `mode`: `VIR|CB|CHQ|LIQ`. Customer payments settle the full remaining amount and mark the invoice paid; **supplier** payments require an explicit `amount`. `account_id` is the bank account id — list them with `scripts/bank-accounts.sh` (`ai_agent_sandbox` now holds `banque lire`). -Emits the new payment id. + +Emits **`{id, bank_transaction_id, num}`**. `bank_transaction_id` is the Dolibarr +bank line (`llx_bank`) the payment created — the id bank reconciliation +(`arcodange-bank-reco`) keys on to link a règlement to a statement line. Put the +**originating bank transaction id** (the Qonto/Wise tx id) in `num`: it is stored +on that bank line (`num_chq`), so the feed reconciles by id rather than by fuzzy +amount/date. Both ends of the reconciliation are therefore captured at write time. ### 4 · Credit note (avoir) — `scripts/creditnote-create.sh` diff --git a/.claude/skills/dolibarr-sandbox-write/scripts/payment-record.sh b/.claude/skills/dolibarr-sandbox-write/scripts/payment-record.sh index 447e8c0..c243171 100755 --- a/.claude/skills/dolibarr-sandbox-write/scripts/payment-record.sh +++ b/.claude/skills/dolibarr-sandbox-write/scripts/payment-record.sh @@ -8,10 +8,14 @@ # account_id (required) the bank account id receiving/paying # date "YYYY-MM-DD" (default today) # amount (REQUIRED for supplier; customer pays the full remaining) -# num, comment (optional) +# num (optional) the bank reference stored on the payment — put the +# Qonto/Wise transaction id here so the règlement reconciles to the feed +# comment (optional) # # The invoice must be VALIDATED first (invoice-create.sh ... "validate":true). -# Emits the new payment id on stdout. +# Emits {id, bank_transaction_id, num} on stdout. `bank_transaction_id` is the +# Dolibarr bank line (llx_bank.fk_bank_line) the payment created — the id that bank +# reconciliation (bank-match) keys on to link this payment to a statement line. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" W="${DOL_WRITE:-${SCRIPT_DIR}/dol-write.sh}" @@ -19,7 +23,8 @@ W="${DOL_WRITE:-${SCRIPT_DIR}/dol-write.sh}" SRC="${1:-}" if [[ -n "${SRC}" && "${SRC}" != "-" ]]; then INPUT="$(cat "${SRC}")"; else INPUT="$(cat)"; fi -PYF="$(mktemp -t dolpy.XXXXXX)"; trap 'rm -f "${PYF}"' EXIT +PYF="$(mktemp -t dolpy.XXXXXX)"; PYF2="$(mktemp -t dolpy2.XXXXXX)" +trap 'rm -f "${PYF}" "${PYF2}"' EXIT cat > "${PYF}" <<'PY' import json, sys, datetime d = json.loads(sys.stdin.read()) @@ -37,24 +42,55 @@ ds = d.get("date") epoch = int((datetime.datetime.strptime(ds, "%Y-%m-%d") if ds else datetime.datetime.now()).timestamp()) inv = d["invoice_id"] +num = d.get("num", "") if supplier: if d.get("amount") is None: sys.exit("payment-record.sh: supplier payments require an 'amount'") endpoint = "/supplierinvoices/%s/payments" % inv body = {"datepaye": epoch, "payment_mode_id": mode, "closepaidinvoices": "yes", "accountid": d["account_id"], - "amount": str(d["amount"]), "num_payment": d.get("num", ""), + "amount": str(d["amount"]), "num_payment": num, "comment": d.get("comment", "")} else: endpoint = "/invoices/%s/payments" % inv body = {"datepaye": epoch, "paymentid": mode, "closepaidinvoices": "yes", - "accountid": d["account_id"], "num_payment": d.get("num", ""), + "accountid": d["account_id"], "num_payment": num, "comment": d.get("comment", "")} print(endpoint) print(json.dumps(body)) +print(num) +PY + +# Correlate the created payment back to its bank transaction line. The payments +# list carries fk_bank_line but not the paiement rowid, so match on the provided +# num (the external bank ref), else fall back to the most recent line. +cat > "${PYF2}" <<'PY' +import json, sys, os +rows = json.load(sys.stdin); rows = rows if isinstance(rows, list) else [] +num = os.environ.get("NUM", ""); pid = int(os.environ["PAYID"]) +pick = None +if num: + cand = [r for r in rows if str(r.get("num", "")) == num] + if cand: + pick = cand[-1] +if pick is None and rows: + pick = max(rows, key=lambda r: r.get("date", "")) +btx = (pick or {}).get("fk_bank_line") +print(json.dumps({"id": pid, + "bank_transaction_id": int(btx) if btx and str(btx).isdigit() else btx, + "num": (pick or {}).get("num", "")})) PY MAPPED="$(printf '%s' "${INPUT}" | python3 "${PYF}")" ENDPOINT="$(sed -n 1p <<<"${MAPPED}")" BODY="$(sed -n 2p <<<"${MAPPED}")" -"${W}" POST "${ENDPOINT}" "${BODY}" +NUM="$(sed -n 3p <<<"${MAPPED}")" + +PAYID="$("${W}" POST "${ENDPOINT}" "${BODY}")" +if [[ ! "${PAYID}" =~ ^[0-9]+$ ]]; then + echo "payment-record.sh: payment POST did not return an id: ${PAYID}" >&2 + exit 1 +fi + +# Same path serves the GET list; resolve fk_bank_line and emit the enriched record. +"${W}" GET "${ENDPOINT}" | PAYID="${PAYID}" NUM="${NUM}" python3 "${PYF2}" diff --git a/bin/arcodange b/bin/arcodange index 92683e7..0d91487 100755 --- a/bin/arcodange +++ b/bin/arcodange @@ -85,7 +85,7 @@ COMMANDS sandbox WRITE ops on erp-sandbox ONLY (host-guarded; JSON on stdin) thirdparty Create a client/supplier fiche invoice Customer/supplier invoice + product/service lines - payment Record a règlement on a validated invoice + payment Record a règlement (num=bank tx id) → bank_transaction_id creditnote Create an avoir — customer or supplier (kind) accounts List bank accounts (id/label) to pick account_id write [body] Raw host-guarded write -- 2.49.1