v1.0.162: auto-classify backend — classify/grouping services, 7 new columns, /classify-batch, /api/groups, /apply-groups
This commit is contained in:
@@ -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}
|
||||
Reference in New Issue
Block a user