Initial import NDC_1C
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
import sys
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from config.client import ODataClient, utc_now_iso
|
||||
from config.settings import LOGS_DIR, load_settings
|
||||
|
||||
|
||||
LINES_ENTITY_SET = "Document_РеализацияТоваровУслуг_Товары"
|
||||
POSTING_ENTITY_SET = "AccountingRegister_Хозрасчетный_RecordType"
|
||||
RECORDER_TYPE = "StandardODATA.Document_РеализацияТоваровУслуг"
|
||||
|
||||
LINE_FIELDS = ["Ref_Key", "LineNumber", "СчетУчета_Key", "Субконто", "Субконто_Type"]
|
||||
POSTING_FIELDS = ["Recorder", "Recorder_Type", "LineNumber", "AccountDr_Key", "AccountCr_Key"]
|
||||
|
||||
|
||||
def _extract_rows(payload: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
rows = payload.get("value")
|
||||
if rows is None and isinstance(payload.get("d"), dict):
|
||||
rows = payload["d"].get("results")
|
||||
if rows is None:
|
||||
return []
|
||||
if isinstance(rows, list):
|
||||
return rows
|
||||
return [rows]
|
||||
|
||||
|
||||
def _safe_read(
|
||||
client: ODataClient,
|
||||
entity_set: str,
|
||||
*,
|
||||
select_fields: list[str],
|
||||
top: int = 200,
|
||||
filter_expr: str | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
params: dict[str, Any] = {"$select": ",".join(select_fields)}
|
||||
if filter_expr:
|
||||
params["$filter"] = filter_expr
|
||||
try:
|
||||
response = client.read_entity_set(entity_set, top=top, extra_params=params)
|
||||
return _extract_rows(response.payload)
|
||||
except Exception as exc:
|
||||
print(f"[warn] read failed for {entity_set} (filter={filter_expr!r}): {exc}")
|
||||
return []
|
||||
|
||||
|
||||
def _group_lines_by_document(rows: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]:
|
||||
grouped: dict[str, list[dict[str, Any]]] = {}
|
||||
for row in rows:
|
||||
ref_key = row.get("Ref_Key")
|
||||
if not isinstance(ref_key, str) or not ref_key:
|
||||
continue
|
||||
grouped.setdefault(ref_key, []).append(row)
|
||||
return grouped
|
||||
|
||||
|
||||
def _pick_document_for_probe(grouped: dict[str, list[dict[str, Any]]]) -> tuple[str | None, list[dict[str, Any]]]:
|
||||
best_doc: str | None = None
|
||||
best_lines: list[dict[str, Any]] = []
|
||||
best_score = -1
|
||||
for doc_key, rows in grouped.items():
|
||||
score = sum(1 for row in rows if row.get("Субконто_Type"))
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_doc = doc_key
|
||||
best_lines = rows
|
||||
return best_doc, best_lines
|
||||
|
||||
|
||||
def _to_line_key(value: Any) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
return str(value)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
settings = load_settings()
|
||||
client = ODataClient(settings)
|
||||
|
||||
line_rows = _safe_read(
|
||||
client,
|
||||
LINES_ENTITY_SET,
|
||||
select_fields=LINE_FIELDS,
|
||||
top=3000,
|
||||
)
|
||||
posting_rows_all = _safe_read(
|
||||
client,
|
||||
POSTING_ENTITY_SET,
|
||||
select_fields=POSTING_FIELDS,
|
||||
top=5000,
|
||||
)
|
||||
|
||||
if not line_rows:
|
||||
print("[error] No document line rows were fetched for probe.")
|
||||
return 1
|
||||
|
||||
if not posting_rows_all:
|
||||
print("[error] No posting rows were fetched for probe.")
|
||||
return 1
|
||||
|
||||
grouped_lines = _group_lines_by_document(line_rows)
|
||||
sales_postings = [
|
||||
row
|
||||
for row in posting_rows_all
|
||||
if row.get("Recorder_Type") == RECORDER_TYPE and isinstance(row.get("Recorder"), str)
|
||||
]
|
||||
|
||||
postings_by_doc: dict[str, list[dict[str, Any]]] = {}
|
||||
for row in sales_postings:
|
||||
recorder = row.get("Recorder")
|
||||
if isinstance(recorder, str) and recorder:
|
||||
postings_by_doc.setdefault(recorder, []).append(row)
|
||||
|
||||
tested_document_key: str | None = None
|
||||
selected_lines: list[dict[str, Any]] = []
|
||||
posting_rows: list[dict[str, Any]] = []
|
||||
best_score = -1
|
||||
|
||||
for doc_key, doc_postings in postings_by_doc.items():
|
||||
doc_lines = grouped_lines.get(doc_key, [])
|
||||
if not doc_lines:
|
||||
continue
|
||||
subconto_typed = sum(1 for row in doc_lines if row.get("Субконто_Type"))
|
||||
score = subconto_typed * 1000 + len(doc_postings)
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
tested_document_key = doc_key
|
||||
selected_lines = doc_lines
|
||||
posting_rows = doc_postings
|
||||
|
||||
if not tested_document_key:
|
||||
tested_document_key, selected_lines = _pick_document_for_probe(grouped_lines)
|
||||
if not tested_document_key:
|
||||
print("[error] No suitable document key found in lines/postings overlap.")
|
||||
return 1
|
||||
posting_rows = postings_by_doc.get(tested_document_key, [])
|
||||
|
||||
postings_by_line: dict[str, list[dict[str, Any]]] = {}
|
||||
for row in posting_rows:
|
||||
key = _to_line_key(row.get("LineNumber"))
|
||||
if key:
|
||||
postings_by_line.setdefault(key, []).append(row)
|
||||
|
||||
joined_rows: list[dict[str, Any]] = []
|
||||
chart_account_subconto_fields: list[str] = []
|
||||
|
||||
for line in selected_lines:
|
||||
line_no = _to_line_key(line.get("LineNumber"))
|
||||
if not line_no:
|
||||
continue
|
||||
|
||||
candidates = postings_by_line.get(line_no, [])
|
||||
if not candidates:
|
||||
continue
|
||||
|
||||
line_account = line.get("СчетУчета_Key")
|
||||
for posting in candidates:
|
||||
account_dr = posting.get("AccountDr_Key")
|
||||
account_cr = posting.get("AccountCr_Key")
|
||||
account_match = bool(
|
||||
isinstance(line_account, str)
|
||||
and line_account
|
||||
and line_account in {account_dr, account_cr}
|
||||
)
|
||||
joined_rows.append(
|
||||
{
|
||||
"line_number": line_no,
|
||||
"recorder": posting.get("Recorder"),
|
||||
"account_dr_key": account_dr,
|
||||
"account_cr_key": account_cr,
|
||||
"line_account_key": line_account,
|
||||
"subconto_value": line.get("Субконто"),
|
||||
"subconto_type": line.get("Субконто_Type"),
|
||||
"account_match": account_match,
|
||||
}
|
||||
)
|
||||
break
|
||||
|
||||
chain_a_status = "derivable" if joined_rows else "opaque"
|
||||
chain_f_status = (
|
||||
"derivable"
|
||||
if any(row.get("subconto_type") for row in joined_rows)
|
||||
else "opaque"
|
||||
)
|
||||
|
||||
report = {
|
||||
"generated_at": utc_now_iso(),
|
||||
"endpoint": settings.service_root,
|
||||
"tested_document_key": tested_document_key,
|
||||
"posting_rows_for_document": len(posting_rows),
|
||||
"line_rows_for_document": len(selected_lines),
|
||||
"joined_rows": len(joined_rows),
|
||||
"joined_sample": joined_rows[:10],
|
||||
"chart_account_subconto_fields": chart_account_subconto_fields,
|
||||
"chain_A_status": chain_a_status,
|
||||
"chain_F_status": chain_f_status,
|
||||
"chain_F_note": (
|
||||
"Derivable by data-driven mapping (Account in posting + Subconto_Type in linked document lines). "
|
||||
"Direct normative mapping from ChartOfAccounts fields is not exposed."
|
||||
),
|
||||
}
|
||||
|
||||
output_path = LOGS_DIR / "deep_subconto_join_probe.json"
|
||||
output_path.write_text(
|
||||
json.dumps(report, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
print(f"[ok] saved: {output_path}")
|
||||
print(
|
||||
f"[ok] chain A={chain_a_status}, chain F={chain_f_status}, "
|
||||
f"joined_rows={len(joined_rows)}"
|
||||
)
|
||||
if chain_a_status != "derivable" or chain_f_status != "derivable":
|
||||
print("[warn] Expected derivable statuses were not reached.")
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user