v1.0.162: auto-classify backend — classify/grouping services, 7 new columns, /classify-batch, /api/groups, /apply-groups

This commit is contained in:
2026-06-24 08:22:21 +04:00
parent 740f75a791
commit 7a96f5b95b
8 changed files with 372 additions and 6 deletions
+95
View File
@@ -0,0 +1,95 @@
"""Grouping service — match classified documents into contract groups."""
import re
from db import documents as db_docs
from db import contracts as db_contracts
from db import supplements as db_supplements
def normalize_number(num):
"""Normalize contract number for matching: uppercase, only letters/digits."""
if not num:
return ""
return re.sub(r"[^A-Z0-9А-Я]", "", num.upper())
def group_documents(batch_id):
"""Group classified documents by contract. Returns list of groups."""
docs = db_docs.list_by_batch(batch_id)
classified = [d for d in docs if d.get("classify_status") == "classified"]
# Separate contracts and supplements
contracts_list = []
supplements_list = []
for d in classified:
if d.get("doc_type") == "contract":
contracts_list.append(d)
else:
supplements_list.append(d)
groups = []
# Each contract becomes a group
for c in contracts_list:
group = {
"contract_number": c.get("own_number") or c.get("filename", ""),
"counterparty": c.get("counterparty") or "",
"documents": [c],
}
# Find supplements matching this contract
c_norm = normalize_number(c.get("own_number"))
for s in supplements_list:
parent = normalize_number(s.get("parent_number") or "")
own = normalize_number(s.get("own_number") or "")
if parent == c_norm or own == c_norm:
if s not in group["documents"]:
group["documents"].append(s)
# Sort by date
group["documents"].sort(key=lambda x: x.get("doc_date") or "")
# Remove matched supplements from the pool
for s in group["documents"]:
if s in supplements_list:
supplements_list.remove(s)
groups.append(group)
# Unmatched documents → "unresolved" group
unmatched = [s for s in supplements_list if s.get("doc_type") != "contract"]
unmatched += [d for d in classified if d.get("classify_status") != "classified"]
if unmatched or len(contracts_list) == 0:
# Put unmatched into a separate group
if unmatched:
groups.append({
"contract_number": "__unresolved__",
"counterparty": "",
"documents": unmatched,
})
return {"ok": True, "groups": groups, "total_docs": len(classified)}
def apply_groups(batch_id, groups_data):
"""Apply confirmed groups: create contracts + supplements."""
created = 0
for g in groups_data:
contract_number = g.get("contract_number", "")
if contract_number == "__unresolved__":
continue
counterparty = g.get("counterparty", "")
docs = g.get("documents", [])
# Create contract
c = db_contracts.insert(contract_number, counterparty)
contract_id = c["id"]
# Create supplements in order
for i, d in enumerate(docs):
doc_id = d.get("id")
supp_type = "initial" if i == 0 else "additional"
db_supplements.insert(contract_id, doc_id, supp_type)
created += 1
return {"ok": True, "created": created}