feat(bank-match): exact tx-id match pass (consumes payment transaction_id)
The consumer side of erp#26/#27: now that a règlement stores its originating bank transaction id (transaction_id -> llx_bank.num_chq), bank-match uses it. New PASS 0 (exact), highest priority, before wire-ref and amt+date: - carry each feed movement's own id (Qonto transaction id; Wise activity + transfer resource id) as feed_ids, and each Dolibarr payment's num. - match when a payment's num equals a feed id. Tagged [tx-id]. - DATE-WINDOW-INDEPENDENT — the id is proof, so it pairs movements whose bank settlement and Dolibarr saisie are weeks apart (which amt+date would miss). Pass 0 runs before the ref index is built, so its matches are excluded from the later passes (no double-match). Fixture-proven: a payment dated 15d off the bank movement (outside the ±7d window) matches via [tx-id] when num carries the Qonto id, and correctly does NOT match when num is empty. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -112,7 +112,10 @@ for t in (json.load(open(os.path.join(work,"qonto.json"))).get("transactions") o
|
||||
amt = float(t.get("amount") or 0)
|
||||
sign = "+" if t.get("side") == "credit" else "-"
|
||||
label = t.get("label") or t.get("operation_type") or "-"
|
||||
qonto_movs.append({"bank":"Qonto", "date":dt, "sign":sign, "amount":amt, "label":label[:40], "op":t.get("operation_type",""), "matched_dol":None, "matched_internal":False})
|
||||
# feed_ids: the Qonto transaction's own id — exact-match key against a Dolibarr
|
||||
# payment whose stored num (num_chq) is that id (set via payment transaction_id).
|
||||
feed_ids = [str(t["id"])] if t.get("id") else []
|
||||
qonto_movs.append({"bank":"Qonto", "date":dt, "sign":sign, "amount":amt, "label":label[:40], "op":t.get("operation_type",""), "feed_ids":feed_ids, "matched_dol":None, "matched_internal":False})
|
||||
|
||||
# 4b. Normalize Wise
|
||||
wise_movs = []
|
||||
@@ -129,7 +132,12 @@ for a in (json.load(open(os.path.join(work,"wise.json"))).get("activities") or [
|
||||
title = strip(a.get("title") or "")[:40]
|
||||
res = a.get("resource") or {}
|
||||
resource_id = str(res.get("id")) if res.get("type") == "TRANSFER" else None
|
||||
wise_movs.append({"bank":"Wise", "date":dt, "sign":sign, "amount":amt, "label":title, "op":typ, "matched_dol":None, "matched_internal":False, "wise_resource_id":resource_id, "wire_ref":""})
|
||||
# feed_ids: the Wise activity id and (for transfers) the resource id — either
|
||||
# may have been stored as the payment's transaction_id; match against both.
|
||||
feed_ids = []
|
||||
if a.get("id"): feed_ids.append(str(a["id"]))
|
||||
if resource_id: feed_ids.append(resource_id)
|
||||
wise_movs.append({"bank":"Wise", "date":dt, "sign":sign, "amount":amt, "label":title, "op":typ, "feed_ids":feed_ids, "matched_dol":None, "matched_internal":False, "wise_resource_id":resource_id, "wire_ref":""})
|
||||
|
||||
# 4b'. If --enrich, load per-transfer wire references and attach to Wise movs
|
||||
if enrich:
|
||||
@@ -165,6 +173,7 @@ for fn in os.listdir(os.path.join(work,"dol_pay")):
|
||||
amt = float(p.get("amount") or 0)
|
||||
dol_pays.append({"side":"customer", "ref":inv["ref"], "date":d, "amount":amt,
|
||||
"fk_account":inv.get("fk_account"), "socid":inv.get("socid"),
|
||||
"num": (p.get("num") or ""),
|
||||
"matched_bank":None, "netted_against":None})
|
||||
|
||||
sup_by_id = {str(r["id"]): r for r in json.load(open(os.path.join(work,"dol_sup.json")))}
|
||||
@@ -177,6 +186,7 @@ for fn in os.listdir(os.path.join(work,"dol_supay")):
|
||||
amt = float(p.get("amount") or 0)
|
||||
dol_pays.append({"side":"supplier", "ref":sup["ref"], "date":d, "amount":amt,
|
||||
"fk_account":sup.get("fk_account"), "socid":sup.get("socid"),
|
||||
"num": (p.get("num") or ""),
|
||||
"matched_bank":None, "netted_against":None})
|
||||
|
||||
# 4d.1. AVOIR cycle netting: an AVC (credit note) for -X on socid S cancels out
|
||||
@@ -204,12 +214,25 @@ for avc in avcs:
|
||||
avc["netted_against"] = partner["ref"]
|
||||
partner["netted_against"] = avc["ref"]
|
||||
|
||||
# 4e. Match — two-pass:
|
||||
# 4e. Match — three-pass, highest confidence first:
|
||||
# PASS 0 (exact) : a Dolibarr payment whose stored num (num_chq = the
|
||||
# transaction_id recorded with the règlement) equals the feed
|
||||
# transaction's own id. Date-window-independent — the id is proof.
|
||||
# PASS 1 (strong) : Wise transfers with an --enrich'd wire reference containing
|
||||
# a "FAC***" pattern try to match the Dolibarr invoice with
|
||||
# that exact ref. This is the highest-confidence match.
|
||||
# a "FAC***" pattern match the Dolibarr invoice with that ref.
|
||||
# PASS 2 (loose) : remaining bank movements use the date+amount heuristic.
|
||||
# Netted Dolibarr entries (avoir cycle) are excluded from both passes.
|
||||
# Netted Dolibarr entries (avoir cycle) are excluded from all passes.
|
||||
|
||||
# Pass 0: exact match on the feed transaction id stored as the payment's num
|
||||
# (num_chq = the transaction_id recorded with the règlement). Date-independent.
|
||||
for m in [x for x in bank_movs if not x["matched_internal"] and not x["matched_dol"] and x.get("feed_ids")]:
|
||||
fids = set(m["feed_ids"])
|
||||
for p in dol_pays:
|
||||
if (p["matched_bank"] is None and p["netted_against"] is None
|
||||
and p.get("num") and str(p["num"]) in fids):
|
||||
m["matched_dol"] = p; m["match_kind"] = "tx-id"
|
||||
p["matched_bank"] = m
|
||||
break
|
||||
|
||||
# Build customer ref -> dol payment index (only un-netted, un-matched entries)
|
||||
ref_index = collections.defaultdict(list)
|
||||
|
||||
Reference in New Issue
Block a user