Files
NODEDC_1C/scripts/deep_probe_subconto.py
T
2026-03-26 10:38:25 +03:00

258 lines
9.4 KiB
Python

from __future__ import annotations
import json
from pathlib import Path
from typing import Any
import xml.etree.ElementTree as ET
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
def _tag_ends(tag: str, suffix: str) -> bool:
return tag.endswith(suffix)
def _parse_metadata(metadata_path: Path) -> tuple[dict[str, list[str]], dict[str, str]]:
root = ET.fromstring(metadata_path.read_text(encoding="utf-8"))
entity_type_props: dict[str, list[str]] = {}
for entity_type in root.iter():
if not _tag_ends(entity_type.tag, "EntityType"):
continue
name = entity_type.attrib.get("Name", "")
if not name:
continue
props: list[str] = []
for child in entity_type:
if _tag_ends(child.tag, "Property"):
prop_name = child.attrib.get("Name", "")
if prop_name:
props.append(prop_name)
entity_type_props[name] = props
entity_set_to_type: dict[str, str] = {}
for entity_set in root.iter():
if not _tag_ends(entity_set.tag, "EntitySet"):
continue
set_name = entity_set.attrib.get("Name", "")
et = entity_set.attrib.get("EntityType", "")
if not set_name or not et:
continue
entity_type_name = et.split(".")[-1]
entity_set_to_type[set_name] = entity_type_name
return entity_type_props, entity_set_to_type
def _entity_sets_with_subconto(
entity_type_props: dict[str, list[str]],
entity_set_to_type: dict[str, str],
) -> list[dict[str, Any]]:
results: list[dict[str, Any]] = []
for entity_set, entity_type in entity_set_to_type.items():
props = entity_type_props.get(entity_type, [])
subconto_props = [p for p in props if "Субконто" in p or "Subconto" in p]
if not subconto_props:
continue
account_props = [p for p in props if p.startswith("Account") or "Счет" in p]
results.append(
{
"entity_set": entity_set,
"entity_type": entity_type,
"subconto_properties": subconto_props,
"account_properties": account_props,
"has_account_and_subconto": bool(account_props),
}
)
results.sort(key=lambda x: x["entity_set"].lower())
return results
def _safe_read_selected(
client: ODataClient,
entity_set: str,
select_fields: list[str] | None = None,
top: int = 20,
) -> dict[str, Any]:
params: dict[str, Any] = {}
if select_fields:
params["$select"] = ",".join(select_fields)
try:
response = client.read_entity_set(entity_set, top=top, extra_params=params or None)
payload = response.payload
rows = payload.get("value")
if rows is None and isinstance(payload.get("d"), dict):
rows = payload["d"].get("results")
if rows is None:
rows = []
if not isinstance(rows, list):
rows = [rows]
return {"status": "ok", "rows": rows}
except Exception as exc:
return {"status": "error", "error": str(exc), "rows": []}
def _non_null_subconto_counts(rows: list[dict[str, Any]]) -> dict[str, int]:
counts: dict[str, int] = {}
for row in rows:
for key, value in row.items():
if "Субконто" not in key and "Subconto" not in key:
continue
if value is None or value == "":
continue
counts[key] = counts.get(key, 0) + 1
return counts
def _load_join_probe_override(logs_dir: Path) -> dict[str, Any]:
path = logs_dir / "deep_subconto_join_probe.json"
if not path.exists():
return {"available": False}
try:
payload = json.loads(path.read_text(encoding="utf-8-sig"))
except Exception as exc:
return {"available": False, "error": str(exc)}
chain_a = payload.get("chain_A_status")
chain_f = payload.get("chain_F_status")
valid = {"derivable", "opaque"}
return {
"available": True,
"path": str(path),
"chain_A_status": chain_a if chain_a in valid else None,
"chain_F_status": chain_f if chain_f in valid else None,
}
def main() -> int:
settings = load_settings()
client = ODataClient(settings)
metadata_path = LOGS_DIR / "metadata.xml"
if not metadata_path.exists():
print("[error] metadata.xml not found. Run fetch_metadata first.")
return 1
entity_type_props, entity_set_to_type = _parse_metadata(metadata_path)
subconto_sets = _entity_sets_with_subconto(entity_type_props, entity_set_to_type)
# Targeted probes for A/F chains
target_sets = [
"AccountingRegister_Хозрасчетный_RecordType",
"AccountingRegister_Хозрасчетный",
"ChartOfAccounts_Хозрасчетный",
"ChartOfCharacteristicTypes_ВидыСубконтоХозрасчетные",
"Document_ОперацияБух",
"Document_ОперацияБух_ТаблицаРегистровБухгалтерии",
"Document_РеализацияТоваровУслуг",
"Document_ПоступлениеТоваровУслуг",
]
target_results: list[dict[str, Any]] = []
for entity_set in target_sets:
if entity_set not in entity_set_to_type:
target_results.append(
{
"entity_set": entity_set,
"status": "missing_in_metadata",
}
)
continue
et = entity_set_to_type[entity_set]
props = entity_type_props.get(et, [])
key_fields = [
p
for p in props
if p in {"Ref_Key", "Recorder", "Recorder_Type", "AccountDr_Key", "AccountCr_Key", "Организация_Key"}
or "Субконто" in p
]
probe = _safe_read_selected(client, entity_set, select_fields=key_fields or None, top=30)
rows = probe.get("rows", [])
target_results.append(
{
"entity_set": entity_set,
"entity_type": et,
"status": probe.get("status"),
"error": probe.get("error"),
"rows_fetched": len(rows),
"selected_fields": key_fields,
"non_null_subconto_counts": _non_null_subconto_counts(rows),
"sample_rows": rows[:3],
}
)
# Chain A/F compact verdict helpers
rec_type = next((x for x in target_results if x["entity_set"] == "AccountingRegister_Хозрасчетный_RecordType"), None)
chart_accounts = next((x for x in target_results if x["entity_set"] == "ChartOfAccounts_Хозрасчетный"), None)
chart_subconto = next((x for x in target_results if x["entity_set"] == "ChartOfCharacteristicTypes_ВидыСубконтоХозрасчетные"), None)
op_buh_tbl = next((x for x in target_results if x["entity_set"] == "Document_ОперацияБух_ТаблицаРегистровБухгалтерии"), None)
chain_a_status = "opaque"
if rec_type and rec_type.get("status") == "ok" and rec_type.get("rows_fetched", 0) > 0:
if rec_type.get("non_null_subconto_counts"):
chain_a_status = "derivable"
elif op_buh_tbl and op_buh_tbl.get("non_null_subconto_counts"):
chain_a_status = "derivable"
chain_f_status = "opaque"
if (
chart_accounts
and chart_accounts.get("status") == "ok"
and chart_accounts.get("rows_fetched", 0) > 0
and chart_subconto
and chart_subconto.get("status") == "ok"
and chart_subconto.get("rows_fetched", 0) > 0
and rec_type
and rec_type.get("status") == "ok"
and rec_type.get("rows_fetched", 0) > 0
):
if rec_type.get("non_null_subconto_counts"):
chain_f_status = "derivable"
join_probe_override = _load_join_probe_override(LOGS_DIR)
if join_probe_override.get("available"):
override_a = join_probe_override.get("chain_A_status")
override_f = join_probe_override.get("chain_F_status")
if override_a == "derivable":
chain_a_status = "derivable"
if override_f == "derivable":
chain_f_status = "derivable"
report = {
"generated_at": utc_now_iso(),
"endpoint": settings.service_root,
"entity_sets_with_subconto_total": len(subconto_sets),
"entity_sets_with_subconto": subconto_sets,
"targeted_results": target_results,
"chain_assessment": {
"A_document_to_posting_account_subconto": chain_a_status,
"F_chart_to_subconto_to_posting": chain_f_status,
},
"override_from_join_probe": join_probe_override,
}
out_path = LOGS_DIR / "deep_subconto_probe.json"
out_path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"[ok] saved: {out_path}")
print(f"[ok] entity_sets_with_subconto_total={len(subconto_sets)}")
print(
"[ok] chain A="
+ report["chain_assessment"]["A_document_to_posting_account_subconto"]
+ ", chain F="
+ report["chain_assessment"]["F_chart_to_subconto_to_posting"]
)
return 0
if __name__ == "__main__":
raise SystemExit(main())