#!/usr/bin/env bash # Create a client and/or supplier thirdparty (fiche tiers) in the SANDBOX — # IDEMPOTENT (erp#44): replaying the same create is a no-op, not a duplicate. # # Input: a JSON object on stdin (or a file path in $1). Fields: # name (required) # role "client" | "supplier" | "both" (default "client") # country_id numeric, default 1 (France) # client_code / supplier_code default "-1" = auto-generate via the code mask # siret, tva_intra, address, zip, town, email, phone, idprof1 (optional) # # Idempotency (erp#44): BEFORE any POST, look the name up on the target with the # same semantics as promote-apply's `#thirdparty:name=...` lookup # (GET /thirdparties?sqlfilters=(t.nom:=:'name'), limit 2): # - 0 matches (the API answers HTTP 404, not []) -> create # - 1 match whose roles cover the requested role -> {"id": , "deduped": true} # - 1 match MISSING the requested role -> ABORT (refuse-never-repair: # silently reusing a client fiche as a supplier would skip the code mask and # hide a data problem — fix the fiche deliberately, not as a create side effect) # - 2+ matches -> ABORT (ambiguous, never guess) # Any other listing failure ABORTS: assuming "no match" on e.g. a 403 would mint # duplicates — the exact failure mode this dedupe exists to prevent. # # Emits {"id": N, "deduped": false} after a create, {"id": N, "deduped": true} # after a dedupe hit. All writes go through dol-write.sh, which refuses any host # that is not the sandbox. # # Examples: # echo '{"name":"KissMetrics","role":"client","tva_intra":"US.."}' | thirdparty-create.sh # thirdparty-create.sh fournisseur.json set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" W="${DOL_WRITE:-${SCRIPT_DIR}/dol-write.sh}" SRC="${1:-}" if [[ -n "${SRC}" && "${SRC}" != "-" ]]; then INPUT="$(cat "${SRC}")"; else INPUT="$(cat)"; fi TMPD="$(mktemp -d -t tpcre.XXXXXX)"; trap 'rm -rf "${TMPD}"' EXIT # --- 1. Validate + map the POST body (before ANY request) ---------------------- cat > "${TMPD}/map.py" <<'PY' import json, sys, urllib.parse d = json.loads(sys.stdin.read()) if not d.get("name"): sys.exit("thirdparty-create.sh: 'name' is required") role = d.get("role", "client").lower() is_client = role in ("client", "both") is_supp = role in ("supplier", "fournisseur", "both") body = { "name": d["name"], "client": "1" if is_client else "0", "fournisseur": "1" if is_supp else "0", "country_id": str(d.get("country_id", 1)), # "-1" => Dolibarr auto-assigns the next code from the mask # (COMPANY_ELEPHANT_MASK_CUSTOMER / _SUPPLIER); "0" when that role is off. "code_client": (d.get("client_code", "-1") if is_client else "0"), "code_fournisseur": (d.get("supplier_code", "-1") if is_supp else "0"), } for k in ("siret", "tva_intra", "address", "zip", "town", "email", "phone", "idprof1"): if d.get(k): body[k] = d[k] # Line 1: POST body. Line 2: the dedupe lookup path (exact name, promote-apply # `#thirdparty:name=` semantics — SQL-escape ' by doubling, then URL-encode). print(json.dumps(body)) flt = "(t.nom:=:'%s')" % str(d["name"]).replace("'", "''") print("/thirdparties?limit=2&sqlfilters=" + urllib.parse.quote(flt)) PY MAPPED="$(printf '%s' "${INPUT}" | python3 "${TMPD}/map.py")" BODY="$(sed -n 1p <<<"${MAPPED}")" LOOKUP="$(sed -n 2p <<<"${MAPPED}")" printf '%s' "${BODY}" > "${TMPD}/body.json" # --- 2. Dedupe by exact name against the target (erp#44) ----------------------- set +e "${W}" GET "${LOOKUP}" > "${TMPD}/list.json" 2> "${TMPD}/list.err" rc=$? set -e if [[ ${rc} -ne 0 ]]; then if grep -q "HTTP 404" "${TMPD}/list.err"; then printf '[]' > "${TMPD}/list.json" # no-match answers 404, not [] (dolibarr skill gotcha) else cat "${TMPD}/list.err" >&2 echo "thirdparty-create.sh: could not look up name on the target — refusing to POST blind (dedupe impossible)" >&2 exit 1 fi fi cat > "${TMPD}/match.py" <<'PY' import json, sys body = json.load(open(sys.argv[1])) try: rows = json.load(open(sys.argv[2])) except Exception: rows = [] rows = rows if isinstance(rows, list) else [] if len(rows) > 1: sys.exit("thirdparty-create.sh: ABORT — name %r matches %d thirdparties on the " "target (ids %s); ambiguous, refusing to guess (same rule as the " "promote '#thirdparty:name=' lookup)" % (body["name"], len(rows), ", ".join(str(r.get("id")) for r in rows))) if rows: r = rows[0] want_client = body["client"] == "1" want_supp = body["fournisseur"] == "1" # Dolibarr: client '1'=customer '2'=prospect '3'=both; fournisseur '1'=yes. has_client = str(r.get("client") or "0") in ("1", "2", "3") has_supp = str(r.get("fournisseur") or "0") == "1" missing = [] if want_client and not has_client: missing.append("client") if want_supp and not has_supp: missing.append("supplier") if missing: sys.exit("thirdparty-create.sh: ABORT — %r already exists (id %s) but " "without the requested role(s): %s. Refusing to dedupe onto a " "fiche that can't carry the ops that follow, and refusing to " "mutate its roles as a create side effect — fix the fiche " "deliberately first." % (body["name"], r.get("id"), ", ".join(missing))) print(json.dumps({"id": int(r["id"]), "deduped": True})) PY MATCH="$(python3 "${TMPD}/match.py" "${TMPD}/body.json" "${TMPD}/list.json")" if [[ -n "${MATCH}" ]]; then echo "thirdparty-create.sh: thirdparty already exists — deduped, no POST" >&2 printf '%s\n' "${MATCH}" exit 0 fi # --- 3. POST the new thirdparty ------------------------------------------------- NEWID="$("${W}" POST /thirdparties "${BODY}")" if [[ ! "${NEWID}" =~ ^[0-9]+$ ]]; then echo "thirdparty-create.sh: create did not return an id: ${NEWID}" >&2 exit 1 fi printf '{"id": %s, "deduped": false}\n' "${NEWID}"