mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-05 19:20:20 +03:00
1464 lines
67 KiB
Python
1464 lines
67 KiB
Python
# meta-info v1.8 — Compact summary of 1C metadata object (Python port)
|
||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||
import argparse
|
||
import os
|
||
import re
|
||
import sys
|
||
|
||
from lxml import etree
|
||
|
||
sys.stdout.reconfigure(encoding="utf-8")
|
||
sys.stderr.reconfigure(encoding="utf-8")
|
||
|
||
# ── arg parsing ──────────────────────────────────────────────
|
||
|
||
parser = argparse.ArgumentParser(allow_abbrev=False)
|
||
parser.add_argument("-ObjectPath", "-Path", required=True)
|
||
parser.add_argument("-Mode", choices=["overview", "brief", "full"], default="overview")
|
||
parser.add_argument("-Name", default="")
|
||
parser.add_argument("-Limit", type=int, default=150)
|
||
parser.add_argument("-Offset", type=int, default=0)
|
||
parser.add_argument("-OutFile", default="")
|
||
args = parser.parse_args()
|
||
|
||
object_path = args.ObjectPath
|
||
mode = args.Mode
|
||
drill_name = args.Name
|
||
limit = args.Limit
|
||
offset = args.Offset
|
||
out_file = args.OutFile
|
||
|
||
# ── output helper ────────────────────────────────────────────
|
||
lines = []
|
||
|
||
|
||
def out(text):
|
||
lines.append(text)
|
||
|
||
|
||
# ── resolve path ─────────────────────────────────────────────
|
||
|
||
if not os.path.isabs(object_path):
|
||
object_path = os.path.join(os.getcwd(), object_path)
|
||
|
||
if os.path.isdir(object_path):
|
||
dir_name = os.path.basename(object_path)
|
||
candidate = os.path.join(object_path, f"{dir_name}.xml")
|
||
sibling = os.path.join(os.path.dirname(object_path), f"{dir_name}.xml")
|
||
if os.path.exists(candidate):
|
||
object_path = candidate
|
||
elif os.path.exists(sibling):
|
||
object_path = sibling
|
||
else:
|
||
xml_files = [f for f in os.listdir(object_path) if f.endswith(".xml")]
|
||
if xml_files:
|
||
object_path = os.path.join(object_path, xml_files[0])
|
||
else:
|
||
print(f"[ERROR] No XML file found in directory: {object_path}")
|
||
sys.exit(1)
|
||
|
||
if not os.path.exists(object_path):
|
||
file_name = os.path.splitext(os.path.basename(object_path))[0]
|
||
parent_dir = os.path.dirname(object_path)
|
||
parent_dir_name = os.path.basename(parent_dir)
|
||
if file_name == parent_dir_name:
|
||
candidate = os.path.join(os.path.dirname(parent_dir), f"{file_name}.xml")
|
||
if os.path.exists(candidate):
|
||
object_path = candidate
|
||
|
||
if not os.path.exists(object_path):
|
||
print(f"[ERROR] File not found: {object_path}")
|
||
sys.exit(1)
|
||
|
||
# ── Load XML ─────────────────────────────────────────────────
|
||
|
||
NS = {
|
||
"md": "http://v8.1c.ru/8.3/MDClasses",
|
||
"v8": "http://v8.1c.ru/8.1/data/core",
|
||
"xr": "http://v8.1c.ru/8.3/xcf/readable",
|
||
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
|
||
"xs": "http://www.w3.org/2001/XMLSchema",
|
||
"cfg": "http://v8.1c.ru/8.1/data/enterprise/current-config",
|
||
"app": "http://v8.1c.ru/8.2/managed-application/core",
|
||
}
|
||
|
||
XSI_NS = NS["xsi"]
|
||
|
||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
||
tree = etree.parse(object_path, parser_xml)
|
||
xml_root = tree.getroot()
|
||
|
||
|
||
def local_name(node):
|
||
return etree.QName(node.tag).localname
|
||
|
||
|
||
def find(parent, xpath):
|
||
if parent is None:
|
||
return None
|
||
r = parent.xpath(xpath, namespaces=NS)
|
||
return r[0] if r else None
|
||
|
||
|
||
def find_all(parent, xpath):
|
||
if parent is None:
|
||
return []
|
||
return parent.xpath(xpath, namespaces=NS)
|
||
|
||
|
||
def inner_text(node):
|
||
if node is None:
|
||
return ""
|
||
return node.text or ""
|
||
|
||
|
||
def text_of(node):
|
||
if node is None:
|
||
return ""
|
||
return (node.text or "").strip()
|
||
|
||
|
||
md_root = find(xml_root, "/md:MetaDataObject") if local_name(xml_root) != "MetaDataObject" else xml_root
|
||
if local_name(xml_root) == "MetaDataObject":
|
||
md_root = xml_root
|
||
else:
|
||
print("[ERROR] Not a valid 1C metadata XML file")
|
||
sys.exit(1)
|
||
|
||
# ── Detect object type ───────────────────────────────────────
|
||
|
||
type_node = None
|
||
md_type = ""
|
||
for child in md_root:
|
||
if not isinstance(child.tag, str):
|
||
continue
|
||
if etree.QName(child.tag).namespace == "http://v8.1c.ru/8.3/MDClasses":
|
||
type_node = child
|
||
md_type = local_name(child)
|
||
break
|
||
|
||
if type_node is None:
|
||
print("[ERROR] Cannot detect metadata type")
|
||
sys.exit(1)
|
||
|
||
# ── Type name maps ───────────────────────────────────────────
|
||
|
||
type_name_map = {
|
||
"Catalog": "Справочник", "Document": "Документ", "Enum": "Перечисление",
|
||
"Constant": "Константа", "InformationRegister": "Регистр сведений",
|
||
"AccumulationRegister": "Регистр накопления", "AccountingRegister": "Регистр бухгалтерии",
|
||
"CalculationRegister": "Регистр расчёта", "ChartOfAccounts": "План счетов",
|
||
"ChartOfCharacteristicTypes": "План видов характеристик",
|
||
"ChartOfCalculationTypes": "План видов расчёта", "BusinessProcess": "Бизнес-процесс",
|
||
"Task": "Задача", "ExchangePlan": "План обмена", "DocumentJournal": "Журнал документов",
|
||
"Report": "Отчёт", "DataProcessor": "Обработка",
|
||
"DefinedType": "Определяемый тип", "CommonModule": "Общий модуль",
|
||
"ScheduledJob": "Регламентное задание", "EventSubscription": "Подписка на событие",
|
||
"HTTPService": "HTTP-сервис", "WebService": "Веб-сервис",
|
||
}
|
||
|
||
ref_type_map = {
|
||
"CatalogRef": "СправочникСсылка", "DocumentRef": "ДокументСсылка",
|
||
"EnumRef": "ПеречислениеСсылка", "ChartOfAccountsRef": "ПланСчетовСсылка",
|
||
"ChartOfCharacteristicTypesRef": "ПВХСсылка", "ChartOfCalculationTypesRef": "ПВРСсылка",
|
||
"ExchangePlanRef": "ПланОбменаСсылка", "BusinessProcessRef": "БизнесПроцессСсылка",
|
||
"TaskRef": "ЗадачаСсылка",
|
||
}
|
||
|
||
reg_type_map = {
|
||
"AccumulationRegister": "РН", "AccountingRegister": "РБ",
|
||
"CalculationRegister": "РР", "InformationRegister": "РС",
|
||
}
|
||
|
||
period_map = {
|
||
"Nonperiodical": "Непериодический", "Day": "День", "Month": "Месяц",
|
||
"Quarter": "Квартал", "Year": "Год", "Second": "Секунда",
|
||
}
|
||
|
||
write_mode_map = {
|
||
"Independent": "независимая", "RecorderSubordinate": "подчинение регистратору",
|
||
}
|
||
|
||
reuse_map = {
|
||
"DontUse": "нет", "DuringRequest": "на время вызова", "DuringSession": "на время сеанса",
|
||
}
|
||
|
||
event_map = {
|
||
"BeforeWrite": "ПередЗаписью", "OnWrite": "ПриЗаписи", "AfterWrite": "ПослеЗаписи",
|
||
"BeforeDelete": "ПередУдалением", "Posting": "ОбработкаПроведения",
|
||
"UndoPosting": "ОбработкаУдаленияПроведения",
|
||
"OnReadAtServer": "ПриЧтенииНаСервере",
|
||
"FillCheckProcessing": "ОбработкаПроверкиЗаполнения",
|
||
}
|
||
|
||
object_type_map = {
|
||
"CatalogObject": "СправочникОбъект", "DocumentObject": "ДокументОбъект",
|
||
"ChartOfAccountsObject": "ПланСчетовОбъект",
|
||
"ChartOfCharacteristicTypesObject": "ПВХОбъект",
|
||
"BusinessProcessObject": "БизнесПроцессОбъект", "TaskObject": "ЗадачаОбъект",
|
||
"ExchangePlanObject": "ПланОбменаОбъект",
|
||
"InformationRegisterRecordSet": "НаборЗаписейРС",
|
||
"AccumulationRegisterRecordSet": "НаборЗаписейРН",
|
||
"AccountingRegisterRecordSet": "НаборЗаписейРБ",
|
||
}
|
||
|
||
number_period_map = {
|
||
"Year": "по году", "Quarter": "по кварталу", "Month": "по месяцу", "Day": "по дню",
|
||
"WholeCatalog": "сквозная",
|
||
}
|
||
|
||
ru_type_name = type_name_map.get(md_type, md_type)
|
||
|
||
# ── Helpers ──────────────────────────────────────────────────
|
||
|
||
|
||
def get_ml_text(node):
|
||
if node is None:
|
||
return ""
|
||
c = find(node, "v8:item[v8:lang='ru']/v8:content")
|
||
if c is not None:
|
||
return inner_text(c)
|
||
c = find(node, "v8:item/v8:content")
|
||
if c is not None:
|
||
return inner_text(c)
|
||
t = text_of(node)
|
||
if t:
|
||
return t
|
||
return ""
|
||
|
||
|
||
# Тип-множество: голое имя метатипа без `.Имя` означает ВСЕ ссылки этого класса
|
||
# (см. docs/meta-dsl-spec.md §«Тип-множество»). Конкретный тип всегда пишется с точкой,
|
||
# поэтому «СправочникСсылка» без точки читается однозначно как обобщённый.
|
||
def format_single_type_set(raw):
|
||
raw = re.sub(r'^d\d+p\d+:', 'cfg:', raw)
|
||
m = re.match(r'^cfg:DefinedType\.(.+)$', raw)
|
||
if m:
|
||
return f"ОпределяемыйТип.{m.group(1)}"
|
||
m = re.match(r'^cfg:Characteristic\.(.+)$', raw)
|
||
if m:
|
||
return f"Характеристика.{m.group(1)}"
|
||
if raw == "cfg:AnyRef":
|
||
return "ЛюбаяСсылка"
|
||
if raw == "cfg:AnyIBRef":
|
||
return "ЛюбаяСсылкаИБ"
|
||
m = re.match(r'^cfg:(\w+Ref)$', raw)
|
||
if m and m.group(1) in ref_type_map:
|
||
return ref_type_map[m.group(1)]
|
||
m = re.match(r'^cfg:(.+)$', raw)
|
||
if m:
|
||
return m.group(1)
|
||
return raw
|
||
|
||
|
||
def format_type(type_node_el):
|
||
if type_node_el is None:
|
||
return ""
|
||
types = []
|
||
for t in find_all(type_node_el, "v8:Type"):
|
||
types.append(format_single_type(inner_text(t), type_node_el))
|
||
for t in find_all(type_node_el, "v8:TypeSet"):
|
||
types.append(format_single_type_set(inner_text(t)))
|
||
if len(types) == 0:
|
||
return ""
|
||
if len(types) == 1:
|
||
return types[0]
|
||
return " | ".join(types)
|
||
|
||
|
||
def format_single_type(raw, parent_node):
|
||
if raw == "xs:string":
|
||
sq = find(parent_node, "v8:StringQualifiers/v8:Length")
|
||
length = inner_text(sq) if sq is not None else ""
|
||
return f"Строка({length})" if length else "Строка"
|
||
if raw == "xs:decimal":
|
||
dg = find(parent_node, "v8:NumberQualifiers/v8:Digits")
|
||
fr = find(parent_node, "v8:NumberQualifiers/v8:FractionDigits")
|
||
d = inner_text(dg) if dg is not None else ""
|
||
f = inner_text(fr) if fr is not None else "0"
|
||
return f"Число({d},{f})" if d else "Число"
|
||
if raw == "xs:boolean":
|
||
return "Булево"
|
||
if raw == "xs:dateTime":
|
||
dq = find(parent_node, "v8:DateQualifiers/v8:DateFractions")
|
||
if dq is not None:
|
||
dv = inner_text(dq)
|
||
if dv == "Date":
|
||
return "Дата"
|
||
if dv == "Time":
|
||
return "Время"
|
||
if dv == "DateTime":
|
||
return "ДатаВремя"
|
||
return "Дата"
|
||
return "ДатаВремя"
|
||
if raw == "v8:ValueStorage":
|
||
return "ХранилищеЗначения"
|
||
if raw == "v8:UUID":
|
||
return "УникальныйИдентификатор"
|
||
if raw == "v8:Null":
|
||
return "Null"
|
||
# Normalize d5p1:/dNpN: → cfg: (both map to same namespace)
|
||
raw = re.sub(r'^d\d+p\d+:', 'cfg:', raw)
|
||
# cfg:CatalogRef.Xxx -> СправочникСсылка.Xxx
|
||
m = re.match(r'^cfg:(\w+)Ref\.(.+)$', raw)
|
||
if m:
|
||
prefix = f"{m.group(1)}Ref"
|
||
objn = m.group(2)
|
||
if prefix in ref_type_map:
|
||
return f"{ref_type_map[prefix]}.{objn}"
|
||
# cfg:EnumRef.Xxx
|
||
m = re.match(r'^cfg:EnumRef\.(.+)$', raw)
|
||
if m:
|
||
return f"ПеречислениеСсылка.{m.group(1)}"
|
||
# cfg:Characteristic.Xxx
|
||
m = re.match(r'^cfg:Characteristic\.(.+)$', raw)
|
||
if m:
|
||
return f"Характеристика.{m.group(1)}"
|
||
# cfg:DefinedType.Xxx
|
||
m = re.match(r'^cfg:DefinedType\.(.+)$', raw)
|
||
if m:
|
||
return f"ОпределяемыйТип.{m.group(1)}"
|
||
# Strip cfg: prefix
|
||
m = re.match(r'^cfg:(.+)$', raw)
|
||
if m:
|
||
return m.group(1)
|
||
return raw
|
||
|
||
|
||
def format_flags(a_props, is_dimension=False):
|
||
flags = []
|
||
fc = find(a_props, "md:FillChecking")
|
||
if fc is not None and inner_text(fc) == "ShowError":
|
||
flags.append("обязательный")
|
||
idx = find(a_props, "md:Indexing")
|
||
if idx is not None:
|
||
iv = inner_text(idx)
|
||
if iv == "Index":
|
||
flags.append("индекс")
|
||
elif iv == "IndexWithAdditionalOrder":
|
||
flags.append("индекс+доп")
|
||
if is_dimension:
|
||
master = find(a_props, "md:Master")
|
||
if master is not None and inner_text(master) == "true":
|
||
flags.append("ведущее")
|
||
ml = find(a_props, "md:MultiLine")
|
||
if ml is not None and inner_text(ml) == "true":
|
||
flags.append("многострочный")
|
||
use = find(a_props, "md:Use")
|
||
if use is not None:
|
||
uv = inner_text(use)
|
||
if uv == "ForFolder":
|
||
flags.append("для папок")
|
||
elif uv == "ForFolderAndItem":
|
||
flags.append("для папок и элементов")
|
||
if not flags:
|
||
return ""
|
||
return f" [{', '.join(flags)}]"
|
||
|
||
|
||
# Ссылка на объект метаданных (`Catalog.Валюты` в Owners и подобных) — не тип значения,
|
||
# но в выводе показываем именно тип, который присваивается: СправочникСсылка.Валюты.
|
||
def format_md_object_ref(raw):
|
||
m = re.match(r'^(\w+)\.(.+)$', raw)
|
||
if m:
|
||
key = f"{m.group(1)}Ref"
|
||
if key in ref_type_map:
|
||
return f"{ref_type_map[key]}.{m.group(2)}"
|
||
return raw
|
||
|
||
|
||
# ── Стандартные реквизиты ────────────────────────────────────
|
||
|
||
# Сколько типов состава печатать списком, прежде чем свернуть в счётчик. По корпусу
|
||
# acc/erp/ut/unf состав ПВХ доходит до 151 типа, при этом 20 из 42 ПВХ укладываются в 5.
|
||
COMPOSED_TYPE_THRESHOLD = 5
|
||
|
||
# Имена полей, состав которых свернули в счётчик, — по ним в конце вывода печатается
|
||
# единственная подсказка, чем состав развернуть.
|
||
collapsed_names = []
|
||
|
||
# Блок StandardAttributes в XML опционален: платформа материализует его только когда хотя бы
|
||
# один стандартный реквизит кастомизирован (docs/meta-dsl-spec.md §7.1.1). Когда блока нет,
|
||
# действуют платформенные дефолты — профиль ниже совпадает с $stdAttrProfile в meta-compile
|
||
# (выведен из корпуса acc+erp). Правки держать синхронными, иначе навыки разъедутся молча.
|
||
std_attr_required_default = {
|
||
"Catalog": {"Owner": True, "Description": True},
|
||
"Document": {"Date": True},
|
||
"ExchangePlan": {"Description": True, "Code": True},
|
||
"ChartOfAccounts": {"Description": True, "Code": True},
|
||
"ChartOfCharacteristicTypes": {"Description": True},
|
||
"ChartOfCalculationTypes": {"Description": True},
|
||
}
|
||
|
||
|
||
def test_std_attr_required(props_node, attr_name, obj_type):
|
||
fc = find(props_node, f"md:StandardAttributes/xr:StandardAttribute[@name='{attr_name}']/xr:FillChecking")
|
||
if fc is not None:
|
||
return inner_text(fc) == "ShowError"
|
||
return std_attr_required_default.get(obj_type, {}).get(attr_name, False)
|
||
|
||
|
||
def new_std_attr(name, type_str, flags):
|
||
f = [x for x in flags if x]
|
||
flag_str = f" [{', '.join(f)}]" if f else ""
|
||
return {"Name": name, "Type": type_str, "Flags": flag_str}
|
||
|
||
|
||
# Стандартные реквизиты, наличие и характеристики которых задаются настройками объекта, —
|
||
# их нельзя вывести из остального вывода, поэтому они попадают и в overview. Ссылка,
|
||
# ПометкаУдаления, Родитель и прочее следуют из типа объекта и строки «Иерархический» —
|
||
# они только в full (см. get_standard_attributes_full).
|
||
def get_standard_attributes(props_node, obj_type, mode_name):
|
||
result = []
|
||
|
||
# Владелец — только у подчинённого справочника; состав типов ниоткуда не выводится
|
||
if obj_type == "Catalog":
|
||
owners_node = find(props_node, "md:Owners")
|
||
owner_types = []
|
||
if owners_node is not None:
|
||
for it in find_all(owners_node, "xr:Item"):
|
||
owner_types.append(format_md_object_ref(inner_text(it)))
|
||
if owner_types:
|
||
req = "обязательный" if test_std_attr_required(props_node, "Owner", obj_type) else None
|
||
result.append(new_std_attr("Владелец", ", ".join(owner_types), [req]))
|
||
|
||
# ТипЗначения ПВХ — до полутора сотен типов, полный список раздул бы сводку в обоих
|
||
# режимах, поэтому сворачиваем в счётчик. Состав смотреть через -Name ТипЗначения.
|
||
if obj_type == "ChartOfCharacteristicTypes":
|
||
vt = find(props_node, "md:Type")
|
||
if vt is not None:
|
||
cnt = len(find_all(vt, "v8:Type")) + len(find_all(vt, "v8:TypeSet"))
|
||
if cnt > COMPOSED_TYPE_THRESHOLD:
|
||
type_str = f"Составной ({cnt})"
|
||
collapsed_names.append("ТипЗначения")
|
||
else:
|
||
type_str = format_type(vt)
|
||
if type_str:
|
||
result.append(new_std_attr("ТипЗначения", type_str, []))
|
||
|
||
# Дата — есть всегда, ценна флагом обязательности
|
||
if obj_type in ("Document", "BusinessProcess", "Task"):
|
||
req = "обязательный" if test_std_attr_required(props_node, "Date", obj_type) else None
|
||
result.append(new_std_attr("Дата", "Дата", [req]))
|
||
|
||
# Номер — тип и длина задаются объектом, при NumberLength=0 номера нет
|
||
if obj_type in ("Document", "BusinessProcess", "Task"):
|
||
num_len = find(props_node, "md:NumberLength")
|
||
if num_len is not None and inner_text(num_len).isdigit() and int(inner_text(num_len)) > 0:
|
||
num_type = find(props_node, "md:NumberType")
|
||
nt_ru = "Число" if num_type is not None and inner_text(num_type) == "Number" else "Строка"
|
||
flags = []
|
||
if test_std_attr_required(props_node, "Number", obj_type):
|
||
flags.append("обязательный")
|
||
num_per = find(props_node, "md:NumberPeriodicity")
|
||
if num_per is not None and inner_text(num_per) in number_period_map:
|
||
flags.append(number_period_map[inner_text(num_per)])
|
||
num_allowed = find(props_node, "md:NumberAllowedLength")
|
||
if num_allowed is not None and inner_text(num_allowed) == "Fixed":
|
||
flags.append("фикс. длина")
|
||
auto_num = find(props_node, "md:Autonumbering")
|
||
if auto_num is not None and inner_text(auto_num) == "true":
|
||
flags.append("авто")
|
||
result.append(new_std_attr("Номер", f"{nt_ru}({inner_text(num_len)})", flags))
|
||
|
||
# Код — при CodeLength=0 кода у объекта нет вовсе, строку не печатаем
|
||
code_len = find(props_node, "md:CodeLength")
|
||
if code_len is not None and inner_text(code_len).isdigit() and int(inner_text(code_len)) > 0:
|
||
code_type = find(props_node, "md:CodeType")
|
||
ct_ru = "Число" if code_type is not None and inner_text(code_type) == "Number" else "Строка"
|
||
flags = []
|
||
if test_std_attr_required(props_node, "Code", obj_type):
|
||
flags.append("обязательный")
|
||
code_allowed = find(props_node, "md:CodeAllowedLength")
|
||
if code_allowed is not None and inner_text(code_allowed) == "Fixed":
|
||
flags.append("фикс. длина")
|
||
result.append(new_std_attr("Код", f"{ct_ru}({inner_text(code_len)})", flags))
|
||
|
||
# Наименование — при DescriptionLength=0 наименования нет
|
||
desc_len = find(props_node, "md:DescriptionLength")
|
||
if desc_len is not None and inner_text(desc_len).isdigit() and int(inner_text(desc_len)) > 0:
|
||
req = "обязательный" if test_std_attr_required(props_node, "Description", obj_type) else None
|
||
result.append(new_std_attr("Наименование", f"Строка({inner_text(desc_len)})", [req]))
|
||
|
||
return result
|
||
|
||
|
||
# Остальные стандартные реквизиты — предсказуемы по типу объекта, но в full полезны
|
||
# как перечень доступных полей для запроса.
|
||
def get_standard_attributes_full(props_node, obj_type, obj_name):
|
||
self_ref_map = {
|
||
"Catalog": "СправочникСсылка",
|
||
"ChartOfCharacteristicTypes": "ПВХСсылка",
|
||
"ChartOfAccounts": "ПланСчетовСсылка",
|
||
"ChartOfCalculationTypes": "ПВРСсылка",
|
||
"ExchangePlan": "ПланОбменаСсылка",
|
||
"Document": "ДокументСсылка",
|
||
"BusinessProcess": "БизнесПроцессСсылка",
|
||
"Task": "ЗадачаСсылка",
|
||
}
|
||
self_ref = f"{self_ref_map[obj_type]}.{obj_name}" if obj_type in self_ref_map else ""
|
||
|
||
result = []
|
||
if self_ref:
|
||
result.append(new_std_attr("Ссылка", self_ref, []))
|
||
result.append(new_std_attr("ПометкаУдаления", "Булево", []))
|
||
|
||
# Родитель и ЭтоГруппа существуют только у иерархических объектов
|
||
hier = find(props_node, "md:Hierarchical")
|
||
is_hier = (hier is not None and inner_text(hier) == "true") or obj_type == "ChartOfAccounts"
|
||
if is_hier and self_ref:
|
||
result.append(new_std_attr("Родитель", self_ref, []))
|
||
ht = find(props_node, "md:HierarchyType")
|
||
if obj_type == "Catalog" and (ht is None or inner_text(ht) == "HierarchyFoldersAndItems"):
|
||
result.append(new_std_attr("ЭтоГруппа", "Булево", []))
|
||
|
||
if obj_type == "Document":
|
||
result.append(new_std_attr("Проведен", "Булево", []))
|
||
elif obj_type == "ExchangePlan":
|
||
result.append(new_std_attr("ЭтотУзел", "Булево", []))
|
||
result.append(new_std_attr("НомерОтправленного", "Число", []))
|
||
result.append(new_std_attr("НомерПринятого", "Число", []))
|
||
elif obj_type == "BusinessProcess":
|
||
result.append(new_std_attr("Стартован", "Булево", []))
|
||
result.append(new_std_attr("Завершен", "Булево", []))
|
||
result.append(new_std_attr("ВедущаяЗадача", "ЗадачаСсылка", []))
|
||
elif obj_type == "Task":
|
||
result.append(new_std_attr("Выполнена", "Булево", []))
|
||
result.append(new_std_attr("БизнесПроцесс", "БизнесПроцессСсылка", []))
|
||
result.append(new_std_attr("ТочкаМаршрута", "БизнесПроцессТочкаМаршрутаСсылка", []))
|
||
elif obj_type == "ChartOfAccounts":
|
||
result.append(new_std_attr("Вид", "ВидСчета", []))
|
||
result.append(new_std_attr("Забалансовый", "Булево", []))
|
||
result.append(new_std_attr("Порядок", "Число", []))
|
||
elif obj_type == "ChartOfCalculationTypes":
|
||
result.append(new_std_attr("ПериодДействияБазовый", "Булево", []))
|
||
|
||
# Предопределённые данные есть у всех перечисленных типов, кроме документов и задач
|
||
if obj_type in ("Catalog", "ChartOfCharacteristicTypes", "ChartOfAccounts", "ChartOfCalculationTypes"):
|
||
result.append(new_std_attr("Предопределенный", "Булево", []))
|
||
result.append(new_std_attr("ИмяПредопределенныхДанных", "Строка", []))
|
||
return result
|
||
|
||
|
||
def get_attributes(parent_node, child_tag="Attribute", is_dimension=False):
|
||
result = []
|
||
for attr in find_all(parent_node, f"md:{child_tag}"):
|
||
aprops = find(attr, "md:Properties")
|
||
if aprops is None:
|
||
continue
|
||
attr_name = inner_text(find(aprops, "md:Name"))
|
||
type_str = format_type(find(aprops, "md:Type"))
|
||
aflags = format_flags(aprops, is_dimension)
|
||
result.append({"Name": attr_name, "Type": type_str, "Flags": aflags, "Props": aprops})
|
||
return result
|
||
|
||
|
||
def get_tabular_sections(parent_node):
|
||
result = []
|
||
for ts in find_all(parent_node, "md:TabularSection"):
|
||
tprops = find(ts, "md:Properties")
|
||
ts_name = inner_text(find(tprops, "md:Name"))
|
||
tchild_objs = find(ts, "md:ChildObjects")
|
||
cols = get_attributes(tchild_objs) if tchild_objs is not None else []
|
||
result.append({"Name": ts_name, "Columns": cols, "ColCount": len(cols)})
|
||
return result
|
||
|
||
|
||
def format_attr_line(attr, max_name_len=30):
|
||
padded = attr["Name"].ljust(max_name_len)
|
||
return f" {padded} {attr['Type']}{attr['Flags']}"
|
||
|
||
|
||
def get_max_name_len(attrs):
|
||
mx = 10
|
||
for a in attrs:
|
||
if len(a["Name"]) > mx:
|
||
mx = len(a["Name"])
|
||
return min(mx + 2, 40)
|
||
|
||
|
||
def get_simple_children(parent_node, tag):
|
||
result = []
|
||
for child in find_all(parent_node, f"md:{tag}"):
|
||
# Form/Template в ChildObjects — простые узлы с именем в тексте, а Command — узел
|
||
# с вложенным Properties: у него inner_text склеил бы всё содержимое в одну строку.
|
||
name_node = find(child, "md:Properties/md:Name")
|
||
result.append(inner_text(name_node) if name_node is not None else inner_text(child))
|
||
return result
|
||
|
||
|
||
def sort_attrs_ref_first(attrs):
|
||
refs = []
|
||
prims = []
|
||
for a in attrs:
|
||
t = a["Type"]
|
||
if (re.search(r'Ссылка\.', t) or re.search(r'Характеристика\.', t) or
|
||
re.search(r'ОпределяемыйТип\.', t) or re.search(r'ПланСчетовСсылка', t) or
|
||
re.search(r'ПВХСсылка', t) or re.search(r'ПВРСсылка', t)):
|
||
refs.append(a)
|
||
else:
|
||
prims.append(a)
|
||
return refs + prims
|
||
|
||
|
||
def decline_cols(n):
|
||
m = n % 10
|
||
h = n % 100
|
||
if 11 <= h <= 19:
|
||
return "колонок"
|
||
if m == 1:
|
||
return "колонка"
|
||
if 2 <= m <= 4:
|
||
return "колонки"
|
||
return "колонок"
|
||
|
||
|
||
def format_source_type(raw):
|
||
raw = re.sub(r'^d\d+p\d+:', 'cfg:', raw)
|
||
m = re.match(r'^cfg:(\w+)\.(.+)$', raw)
|
||
if m:
|
||
prefix = m.group(1)
|
||
name = m.group(2)
|
||
if prefix in object_type_map:
|
||
return f"{object_type_map[prefix]}.{name}"
|
||
m = re.match(r'^cfg:(.+)$', raw)
|
||
if m:
|
||
return m.group(1)
|
||
return raw
|
||
|
||
|
||
def get_http_endpoints(child_objs):
|
||
result = []
|
||
for tpl in find_all(child_objs, "md:URLTemplate"):
|
||
tp = find(tpl, "md:Properties")
|
||
tpl_name = inner_text(find(tp, "md:Name"))
|
||
template = inner_text(find(tp, "md:Template"))
|
||
methods = []
|
||
tpl_co = find(tpl, "md:ChildObjects")
|
||
if tpl_co is not None:
|
||
for m in find_all(tpl_co, "md:Method"):
|
||
mp = find(m, "md:Properties")
|
||
http_method = inner_text(find(mp, "md:HTTPMethod"))
|
||
handler = inner_text(find(mp, "md:Handler"))
|
||
methods.append({"HTTPMethod": http_method, "Handler": handler,
|
||
"Name": inner_text(find(mp, "md:Name"))})
|
||
result.append({"Name": tpl_name, "Template": template, "Methods": methods})
|
||
return result
|
||
|
||
|
||
def get_ws_operations(child_objs):
|
||
result = []
|
||
for op in find_all(child_objs, "md:Operation"):
|
||
oprops = find(op, "md:Properties")
|
||
op_name = inner_text(find(oprops, "md:Name"))
|
||
ret_type_el = find(oprops, "md:XDTOReturningValueType")
|
||
ret_str = inner_text(ret_type_el) if ret_type_el is not None and inner_text(ret_type_el) else "void"
|
||
proc_name_el = find(oprops, "md:ProcedureName")
|
||
params = []
|
||
op_co = find(op, "md:ChildObjects")
|
||
if op_co is not None:
|
||
for p in find_all(op_co, "md:Parameter"):
|
||
pp = find(p, "md:Properties")
|
||
p_name = inner_text(find(pp, "md:Name"))
|
||
p_type_el = find(pp, "md:XDTOValueType")
|
||
p_type_str = inner_text(p_type_el) if p_type_el is not None else "?"
|
||
dir_el = find(pp, "md:TransferDirection")
|
||
dir_str = f" [{inner_text(dir_el).lower()}]" if dir_el is not None and inner_text(dir_el) != "In" else ""
|
||
params.append(f"{p_name}: {p_type_str}{dir_str}")
|
||
param_str = ", ".join(params)
|
||
result.append({"Name": op_name, "Params": param_str, "ReturnType": ret_str,
|
||
"ProcName": inner_text(proc_name_el) if proc_name_el is not None else ""})
|
||
return result
|
||
|
||
|
||
# ── Support status of this object (Ext/ParentConfigurations.bin) ──
|
||
# See docs/1c-support-state-spec.md. Walks up to the config root, decodes the
|
||
# object's support rule. Never throws — degrades to "не на поддержке".
|
||
def _meta_is_external_root(xml_path):
|
||
if not os.path.isfile(xml_path):
|
||
return False
|
||
try:
|
||
mx = etree.parse(xml_path).getroot()
|
||
for child in mx:
|
||
if isinstance(child.tag, str):
|
||
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
|
||
except Exception:
|
||
return False
|
||
return False
|
||
|
||
|
||
def get_object_support_status(obj_uuid):
|
||
try:
|
||
if _meta_is_external_root(object_path):
|
||
return None
|
||
d = os.path.dirname(object_path)
|
||
bin_path = None
|
||
for _ in range(8):
|
||
if not d:
|
||
break
|
||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
||
bin_path = cand
|
||
break
|
||
parent = os.path.dirname(d)
|
||
if parent == d:
|
||
break
|
||
d = parent
|
||
if not bin_path or not os.path.exists(bin_path):
|
||
return "не на поддержке"
|
||
data = open(bin_path, "rb").read()
|
||
if len(data) <= 32:
|
||
return "снято с поддержки (правки свободны)"
|
||
if data[:3] == b"\xef\xbb\xbf":
|
||
data = data[3:]
|
||
text = data.decode("utf-8", "replace")
|
||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
||
if not h:
|
||
return "не на поддержке"
|
||
g = int(h.group(1))
|
||
k = int(h.group(2))
|
||
if k == 0:
|
||
return "снято с поддержки (правки свободны)"
|
||
if g == 1:
|
||
return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения"
|
||
if not obj_uuid:
|
||
return "не на поддержке"
|
||
best = None
|
||
for m in re.finditer(r"([0-2]),0," + re.escape(obj_uuid.lower()), text):
|
||
f1 = int(m.group(1))
|
||
if best is None or f1 < best:
|
||
best = f1
|
||
if best is None:
|
||
return "не на поддержке"
|
||
return {
|
||
0: "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта",
|
||
1: "редактируется с сохранением поддержки",
|
||
2: "снято с поддержки (правки свободны)",
|
||
}.get(best, "не на поддержке")
|
||
except Exception:
|
||
return "не на поддержке"
|
||
|
||
|
||
# ── Extract metadata ─────────────────────────────────────────
|
||
|
||
props = find(type_node, "md:Properties")
|
||
child_objs = find(type_node, "md:ChildObjects")
|
||
obj_name = inner_text(find(props, "md:Name"))
|
||
syn_node = find(props, "md:Synonym")
|
||
synonym = get_ml_text(syn_node)
|
||
|
||
# Presentations (type-choice dialogs show "Представление объекта" as the ref type name)
|
||
obj_presentation = get_ml_text(find(props, "md:ObjectPresentation"))
|
||
ext_obj_presentation = get_ml_text(find(props, "md:ExtendedObjectPresentation"))
|
||
list_presentation = get_ml_text(find(props, "md:ListPresentation"))
|
||
ext_list_presentation = get_ml_text(find(props, "md:ExtendedListPresentation"))
|
||
|
||
# Reference (ref-typed) metadata objects — those with a ...Ref type
|
||
ref_md_types = {"Catalog", "Document", "Enum", "ChartOfAccounts",
|
||
"ChartOfCharacteristicTypes", "ChartOfCalculationTypes",
|
||
"ExchangePlan", "BusinessProcess", "Task"}
|
||
is_ref_object = md_type in ref_md_types
|
||
|
||
# Effective type presentation: ObjectPresentation -> Synonym -> Name
|
||
type_presentation = obj_presentation or synonym or obj_name
|
||
|
||
# ── Handle -Name drill-down ──────────────────────────────────
|
||
|
||
drill_done = False
|
||
|
||
if drill_name and child_objs is not None:
|
||
# Search in attributes/dimensions/resources
|
||
attr_tags = ["Attribute", "Dimension", "Resource"]
|
||
for tag in attr_tags:
|
||
if drill_done:
|
||
break
|
||
for attr in find_all(child_objs, f"md:{tag}"):
|
||
ap = find(attr, "md:Properties")
|
||
if ap is None:
|
||
continue
|
||
an = inner_text(find(ap, "md:Name"))
|
||
if an == drill_name:
|
||
tag_ru = {"Attribute": "Реквизит", "Dimension": "Измерение", "Resource": "Ресурс"}[tag]
|
||
out(f"{tag_ru}: {an}")
|
||
type_str = format_type(find(ap, "md:Type"))
|
||
out(f" Тип: {type_str}")
|
||
fc = find(ap, "md:FillChecking")
|
||
out(f" Обязательный: {'да' if fc is not None and inner_text(fc) == 'ShowError' else 'нет'}")
|
||
idx = find(ap, "md:Indexing")
|
||
idx_val = inner_text(idx) if idx is not None else "DontIndex"
|
||
if idx_val == "DontIndex" or not idx_val:
|
||
idx_ru = "нет"
|
||
elif idx_val == "Index":
|
||
idx_ru = "Индекс"
|
||
elif idx_val == "IndexWithAdditionalOrder":
|
||
idx_ru = "Индекс с доп. упорядочиванием"
|
||
else:
|
||
idx_ru = idx_val
|
||
out(f" Индексирование: {idx_ru}")
|
||
ml = find(ap, "md:MultiLine")
|
||
if ml is not None and inner_text(ml) == "true":
|
||
out(" Многострочный: да")
|
||
use = find(ap, "md:Use")
|
||
if use is not None and inner_text(use) != "ForItem":
|
||
use_val = inner_text(use)
|
||
use_ru = {"ForFolder": "для папок", "ForFolderAndItem": "для папок и элементов"}.get(use_val, use_val)
|
||
out(f" Использование: {use_ru}")
|
||
fv = find(ap, "md:FillValue")
|
||
if fv is not None and fv.get(f"{{{XSI_NS}}}nil") != "true" and inner_text(fv):
|
||
fv_text = inner_text(fv)
|
||
if fv_text.endswith(".EmptyRef"):
|
||
fv_text = "Пустая ссылка"
|
||
elif fv_text == "false":
|
||
fv_text = "Ложь"
|
||
elif fv_text == "true":
|
||
fv_text = "Истина"
|
||
out(f" Значение заполнения: {fv_text}")
|
||
else:
|
||
out(" Значение заполнения: \u2014")
|
||
if tag == "Dimension":
|
||
master = find(ap, "md:Master")
|
||
out(f" Ведущее: {'да' if master is not None and inner_text(master) == 'true' else 'нет'}")
|
||
mf = find(ap, "md:MainFilter")
|
||
out(f" Основной отбор: {'да' if mf is not None and inner_text(mf) == 'true' else 'нет'}")
|
||
syn_a = find(ap, "md:Synonym")
|
||
syn_text = get_ml_text(syn_a)
|
||
if syn_text and syn_text != an:
|
||
out(f" Синоним: {syn_text}")
|
||
drill_done = True
|
||
break
|
||
|
||
# Search in tabular sections
|
||
if not drill_done:
|
||
for ts in find_all(child_objs, "md:TabularSection"):
|
||
tp = find(ts, "md:Properties")
|
||
tn = inner_text(find(tp, "md:Name"))
|
||
if tn == drill_name:
|
||
ts_co = find(ts, "md:ChildObjects")
|
||
cols = get_attributes(ts_co) if ts_co is not None else []
|
||
out(f"ТЧ: {tn} ({len(cols)} {decline_cols(len(cols))}):")
|
||
if cols:
|
||
ml = get_max_name_len(cols)
|
||
for c in cols:
|
||
out(format_attr_line(c, ml))
|
||
drill_done = True
|
||
break
|
||
|
||
# Search in enum values
|
||
if not drill_done:
|
||
for ev in find_all(child_objs, "md:EnumValue"):
|
||
ep = find(ev, "md:Properties")
|
||
en = inner_text(find(ep, "md:Name"))
|
||
if en == drill_name:
|
||
syn_e = find(ep, "md:Synonym")
|
||
syn_text = get_ml_text(syn_e)
|
||
out(f"Значение перечисления: {en}")
|
||
if syn_text:
|
||
out(f' Синоним: "{syn_text}"')
|
||
cm = find(ep, "md:Comment")
|
||
if cm is not None and inner_text(cm):
|
||
out(f" Комментарий: {inner_text(cm)}")
|
||
drill_done = True
|
||
break
|
||
|
||
# Search in HTTPService URLTemplates
|
||
if not drill_done and md_type == "HTTPService":
|
||
for tpl in find_all(child_objs, "md:URLTemplate"):
|
||
tp = find(tpl, "md:Properties")
|
||
if inner_text(find(tp, "md:Name")) == drill_name:
|
||
template = inner_text(find(tp, "md:Template"))
|
||
out(f"Шаблон URL: {drill_name}")
|
||
out(f" Путь: {template}")
|
||
tpl_co = find(tpl, "md:ChildObjects")
|
||
if tpl_co is not None:
|
||
for m in find_all(tpl_co, "md:Method"):
|
||
mp = find(m, "md:Properties")
|
||
http_method = inner_text(find(mp, "md:HTTPMethod"))
|
||
handler = inner_text(find(mp, "md:Handler"))
|
||
out(f" {http_method} \u2192 {handler}")
|
||
drill_done = True
|
||
break
|
||
|
||
# Search in WebService Operations
|
||
if not drill_done and md_type == "WebService":
|
||
for op in find_all(child_objs, "md:Operation"):
|
||
oprops = find(op, "md:Properties")
|
||
if inner_text(find(oprops, "md:Name")) == drill_name:
|
||
out(f"Операция: {drill_name}")
|
||
ret_type_el = find(oprops, "md:XDTOReturningValueType")
|
||
out(f" Возвращает: {inner_text(ret_type_el) if ret_type_el is not None and inner_text(ret_type_el) else 'void'}")
|
||
proc_name_el = find(oprops, "md:ProcedureName")
|
||
if proc_name_el is not None and inner_text(proc_name_el):
|
||
out(f" Процедура: {inner_text(proc_name_el)}")
|
||
comment_el = find(oprops, "md:Comment")
|
||
if comment_el is not None and inner_text(comment_el):
|
||
out(f" Комментарий: {inner_text(comment_el)}")
|
||
op_co = find(op, "md:ChildObjects")
|
||
if op_co is not None:
|
||
params_els = find_all(op_co, "md:Parameter")
|
||
if params_els:
|
||
out(" Параметры:")
|
||
for p in params_els:
|
||
pp = find(p, "md:Properties")
|
||
p_name = inner_text(find(pp, "md:Name"))
|
||
p_type_el = find(pp, "md:XDTOValueType")
|
||
dir_el = find(pp, "md:TransferDirection")
|
||
dir_str = f" [{inner_text(dir_el).lower()}]" if dir_el is not None and inner_text(dir_el) != "In" else ""
|
||
out(f" {p_name}: {inner_text(p_type_el) if p_type_el is not None else '?'}{dir_str}")
|
||
drill_done = True
|
||
break
|
||
|
||
# Не нашли среди дочерних объектов — пробуем стандартные реквизиты ниже
|
||
|
||
# Drill-down по стандартному реквизиту — единственный способ увидеть состав типов целиком,
|
||
# когда в сводке он свёрнут в счётчик. Живёт в Properties, а не в ChildObjects, поэтому идёт
|
||
# отдельной веткой и работает даже у объектов без ChildObjects.
|
||
if drill_name and not drill_done:
|
||
std_all = get_standard_attributes(props, md_type, "full") + get_standard_attributes_full(props, md_type, obj_name)
|
||
for s in std_all:
|
||
if s["Name"] != drill_name:
|
||
continue
|
||
out(f"Стандартный реквизит: {s['Name']}")
|
||
|
||
# Состав типов разворачиваем списком: ради этого drill-down и нужен
|
||
type_list = []
|
||
type_src = find(props, "md:Type") if drill_name == "ТипЗначения" else None
|
||
if type_src is not None:
|
||
for t in find_all(type_src, "v8:Type"):
|
||
type_list.append(format_single_type(inner_text(t), type_src))
|
||
for t in find_all(type_src, "v8:TypeSet"):
|
||
type_list.append(format_single_type_set(inner_text(t)))
|
||
elif drill_name == "Владелец":
|
||
owners_node = find(props, "md:Owners")
|
||
if owners_node is not None:
|
||
for it in find_all(owners_node, "xr:Item"):
|
||
type_list.append(format_md_object_ref(inner_text(it)))
|
||
|
||
if type_list:
|
||
out(f" Типы ({len(type_list)}):")
|
||
for t in type_list:
|
||
out(f" {t}")
|
||
else:
|
||
out(f" Тип: {s['Type']}")
|
||
flag_text = s["Flags"].strip()
|
||
if flag_text:
|
||
out(f" Свойства: {flag_text.strip('[]')}")
|
||
drill_done = True
|
||
break
|
||
|
||
if not drill_done:
|
||
print(f"[ERROR] '{drill_name}' not found in {obj_name}")
|
||
sys.exit(1)
|
||
|
||
# ── Main output (not drill-down) ─────────────────────────────
|
||
|
||
if not drill_done:
|
||
# Build header
|
||
header = f"=== {ru_type_name}: {obj_name}"
|
||
if synonym and synonym != obj_name:
|
||
header += f' \u2014 "{synonym}"'
|
||
header += " ==="
|
||
out(header)
|
||
_support = get_object_support_status(type_node.get('uuid', ''))
|
||
if _support is not None:
|
||
out(f"Поддержка: {_support}")
|
||
|
||
# Type presentation (ref objects)
|
||
if is_ref_object:
|
||
out(f"Представление типа: {type_presentation}")
|
||
if mode == "full":
|
||
if obj_presentation:
|
||
out(f"Представление объекта: {obj_presentation}")
|
||
if ext_obj_presentation:
|
||
out(f"Расширенное представление объекта: {ext_obj_presentation}")
|
||
if list_presentation:
|
||
out(f"Представление списка: {list_presentation}")
|
||
if ext_list_presentation:
|
||
out(f"Расширенное представление списка: {ext_list_presentation}")
|
||
|
||
if mode == "brief":
|
||
# Подчинённость — единственный факт структуры, который из brief не выводится никак,
|
||
# а без него код записи элемента падает. Про обязательность здесь намеренно молчим:
|
||
# обязательными бывают и обычные реквизиты, а их brief не разбирает.
|
||
if md_type == "Catalog":
|
||
owners_node = find(props, "md:Owners")
|
||
owner_names = []
|
||
if owners_node is not None:
|
||
for it in find_all(owners_node, "xr:Item"):
|
||
owner_names.append(re.sub(r'^\w+\.', '', inner_text(it)))
|
||
if owner_names:
|
||
out(f"Подчинён: {', '.join(owner_names)}")
|
||
|
||
# Attributes
|
||
attrs = get_attributes(child_objs) if child_objs is not None else []
|
||
if attrs:
|
||
names = ", ".join(a["Name"] for a in attrs)
|
||
out(f"Реквизиты ({len(attrs)}): {names}")
|
||
|
||
# Dimensions/Resources for registers
|
||
if md_type.endswith("Register"):
|
||
dims = get_attributes(child_objs, "Dimension", True) if child_objs is not None else []
|
||
if dims:
|
||
names = ", ".join(d["Name"] for d in dims)
|
||
out(f"Измерения ({len(dims)}): {names}")
|
||
res = get_attributes(child_objs, "Resource") if child_objs is not None else []
|
||
if res:
|
||
names = ", ".join(r["Name"] for r in res)
|
||
out(f"Ресурсы ({len(res)}): {names}")
|
||
|
||
# Tabular sections
|
||
tss = get_tabular_sections(child_objs) if child_objs is not None else []
|
||
if tss:
|
||
ts_parts = [f"{t['Name']}({t['ColCount']})" for t in tss]
|
||
out(f"ТЧ ({len(tss)}): {', '.join(ts_parts)}")
|
||
|
||
# Enum values
|
||
if md_type == "Enum" and child_objs is not None:
|
||
vals = []
|
||
for ev in find_all(child_objs, "md:EnumValue"):
|
||
ep = find(ev, "md:Properties")
|
||
vals.append(inner_text(find(ep, "md:Name")))
|
||
if vals:
|
||
out(f"Значения ({len(vals)}): {', '.join(vals)}")
|
||
|
||
# DefinedType brief
|
||
if md_type == "DefinedType":
|
||
type_node2 = find(props, "md:Type")
|
||
if type_node2 is not None:
|
||
types = []
|
||
for t in find_all(type_node2, "v8:Type"):
|
||
types.append(format_single_type(inner_text(t), type_node2))
|
||
if types:
|
||
out(f"Типы ({len(types)}): {', '.join(types)}")
|
||
|
||
# CommonModule brief
|
||
if md_type == "CommonModule":
|
||
flags = []
|
||
for flag_name, flag_label in [("Global", "Глобальный"), ("Server", "Сервер"),
|
||
("ServerCall", "Вызов сервера"),
|
||
("ClientManagedApplication", "Клиент управляемое"),
|
||
("ClientOrdinaryApplication", "Обычный клиент"),
|
||
("ExternalConnection", "Внешнее соединение"),
|
||
("Privileged", "Привилегированный")]:
|
||
n = find(props, f"md:{flag_name}")
|
||
if n is not None and inner_text(n) == "true":
|
||
flags.append(flag_label)
|
||
reuse = find(props, "md:ReturnValuesReuse")
|
||
if reuse is not None and inner_text(reuse) != "DontUse":
|
||
reuse_ru = reuse_map.get(inner_text(reuse), inner_text(reuse))
|
||
flags.append(f"Повторное использование: {reuse_ru}")
|
||
if flags:
|
||
out(" | ".join(flags))
|
||
|
||
# ScheduledJob brief
|
||
if md_type == "ScheduledJob":
|
||
method = find(props, "md:MethodName")
|
||
if method is not None and inner_text(method):
|
||
m_name = inner_text(method)
|
||
m2 = re.match(r'^CommonModule\.(.+)$', m_name)
|
||
if m2:
|
||
m_name = m2.group(1)
|
||
out(f"Метод: {m_name}")
|
||
sj_parts = []
|
||
use = find(props, "md:Use")
|
||
sj_parts.append(f"Использование: {'да' if use is not None and inner_text(use) == 'true' else 'нет'}")
|
||
predef = find(props, "md:Predefined")
|
||
sj_parts.append(f"Предопределённое: {'да' if predef is not None and inner_text(predef) == 'true' else 'нет'}")
|
||
restart_cnt = find(props, "md:RestartCountOnFailure")
|
||
restart_int = find(props, "md:RestartIntervalOnFailure")
|
||
if restart_cnt is not None and inner_text(restart_cnt).isdigit() and int(inner_text(restart_cnt)) > 0:
|
||
sj_parts.append(f"Перезапуск: {inner_text(restart_cnt)} (через {inner_text(restart_int)} сек)")
|
||
out(" | ".join(sj_parts))
|
||
|
||
# EventSubscription brief
|
||
if md_type == "EventSubscription":
|
||
es_parts = []
|
||
event = find(props, "md:Event")
|
||
if event is not None and inner_text(event):
|
||
ev_ru = event_map.get(inner_text(event), inner_text(event))
|
||
es_parts.append(f"Событие: {ev_ru}")
|
||
handler = find(props, "md:Handler")
|
||
if handler is not None and inner_text(handler):
|
||
h_name = inner_text(handler)
|
||
m2 = re.match(r'^CommonModule\.(.+)$', h_name)
|
||
if m2:
|
||
h_name = m2.group(1)
|
||
es_parts.append(f"Обработчик: {h_name}")
|
||
source = find(props, "md:Source")
|
||
if source is not None:
|
||
src_count = len(find_all(source, "v8:Type"))
|
||
if src_count > 0:
|
||
es_parts.append(f"Источники: {src_count}")
|
||
if es_parts:
|
||
out(" | ".join(es_parts))
|
||
|
||
# HTTPService brief
|
||
if md_type == "HTTPService":
|
||
root_url = find(props, "md:RootURL")
|
||
if root_url is not None and inner_text(root_url):
|
||
out(f"Корневой URL: /{inner_text(root_url)}")
|
||
if child_objs is not None:
|
||
endpoints = get_http_endpoints(child_objs)
|
||
if endpoints:
|
||
total_methods = sum(len(ep["Methods"]) for ep in endpoints)
|
||
out(f"Шаблоны: {len(endpoints)} | Методы: {total_methods}")
|
||
|
||
# WebService brief
|
||
if md_type == "WebService":
|
||
ns_url = find(props, "md:Namespace")
|
||
if ns_url is not None and inner_text(ns_url):
|
||
out(f"Пространство имён: {inner_text(ns_url)}")
|
||
if child_objs is not None:
|
||
ops = get_ws_operations(child_objs)
|
||
if ops:
|
||
out(f"Операции: {len(ops)}")
|
||
|
||
else:
|
||
# mode: overview / full
|
||
|
||
# Document-specific header
|
||
if md_type == "Document":
|
||
posting = find(props, "md:Posting")
|
||
parts = []
|
||
# Номер уехал в блок стандартных реквизитов — здесь остаются свойства объекта
|
||
if posting is not None:
|
||
parts.append(f"Проведение: {'да' if inner_text(posting) == 'Allow' else 'нет'}")
|
||
if parts:
|
||
out(" | ".join(parts))
|
||
|
||
# Свойства иерархии и подчинения. Иерархия — не только у справочников: свойство
|
||
# Hierarchical есть и у ПВХ (в корпусе acc/erp/ut/unf их 13), а без этой строки
|
||
# в full появлялся Родитель, происхождение которого было ниоткуда не видно.
|
||
parts = []
|
||
hier = find(props, "md:Hierarchical")
|
||
if hier is not None and inner_text(hier) == "true":
|
||
ht = find(props, "md:HierarchyType")
|
||
ht_text = "группы и элементы" if ht is not None and inner_text(ht) == "HierarchyFoldersAndItems" else "элементы"
|
||
limit_node = find(props, "md:LimitLevelCount")
|
||
level_node = find(props, "md:LevelCount")
|
||
if limit_node is not None and inner_text(limit_node) == "true" and level_node is not None:
|
||
ht_text += f", уровней: {inner_text(level_node)}"
|
||
else:
|
||
ht_text += ", без ограничения уровней"
|
||
parts.append(f"Иерархический: {ht_text}")
|
||
# Код и Наименование уехали в блок стандартных реквизитов — здесь только свойства объекта.
|
||
# Подчинение печатаем лишь когда оно отличается от дефолта ToItems.
|
||
if md_type == "Catalog":
|
||
sub = find(props, "md:SubordinationUse")
|
||
if sub is not None and inner_text(sub) != "ToItems":
|
||
sub_ru = {"ToFolders": "группам", "ToFoldersAndItems": "группам и элементам"}.get(
|
||
inner_text(sub), inner_text(sub))
|
||
parts.append(f"Подчинение: {sub_ru}")
|
||
if parts:
|
||
out(" | ".join(parts))
|
||
|
||
# Register-specific header
|
||
if md_type.endswith("Register"):
|
||
parts = []
|
||
if md_type == "InformationRegister":
|
||
per = find(props, "md:InformationRegisterPeriodicity")
|
||
if per is not None:
|
||
per_ru = period_map.get(inner_text(per), inner_text(per))
|
||
parts.append(f"Периодичность: {per_ru}")
|
||
wm = find(props, "md:WriteMode")
|
||
if wm is not None:
|
||
wm_ru = write_mode_map.get(inner_text(wm), inner_text(wm))
|
||
parts.append(f"Запись: {wm_ru}")
|
||
if md_type == "AccumulationRegister":
|
||
reg_kind = find(props, "md:RegisterType")
|
||
if reg_kind is not None:
|
||
rk_val = inner_text(reg_kind)
|
||
rk_ru = {"Balances": "остатки", "Turnovers": "обороты"}.get(rk_val, rk_val)
|
||
parts.append(f"Вид: {rk_ru}")
|
||
if parts:
|
||
out(" | ".join(parts))
|
||
|
||
# Constant
|
||
if md_type == "Constant":
|
||
type_str = format_type(find(props, "md:Type"))
|
||
if type_str:
|
||
out(f"Тип: {type_str}")
|
||
|
||
# Report: MainDataCompositionSchema
|
||
if md_type == "Report":
|
||
main_dcs = find(props, "md:MainDataCompositionSchema")
|
||
if main_dcs is not None and inner_text(main_dcs):
|
||
dcs_name = inner_text(main_dcs)
|
||
m2 = re.search(r'\.Template\.(.+)$', dcs_name)
|
||
if m2:
|
||
dcs_name = m2.group(1)
|
||
out(f"Основная СКД: {dcs_name}")
|
||
|
||
# DefinedType
|
||
if md_type == "DefinedType":
|
||
type_node2 = find(props, "md:Type")
|
||
if type_node2 is not None:
|
||
types = []
|
||
for t in find_all(type_node2, "v8:Type"):
|
||
types.append(format_single_type(inner_text(t), type_node2))
|
||
if types:
|
||
out(f"Типы ({len(types)}):")
|
||
for t in types:
|
||
out(f" {t}")
|
||
|
||
# CommonModule
|
||
if md_type == "CommonModule":
|
||
flags = []
|
||
for flag_name, flag_label in [("Global", "Глобальный"), ("Server", "Сервер"),
|
||
("ServerCall", "Вызов сервера"),
|
||
("ClientManagedApplication", "Клиент управляемое"),
|
||
("ClientOrdinaryApplication", "Обычный клиент"),
|
||
("ExternalConnection", "Внешнее соединение"),
|
||
("Privileged", "Привилегированный")]:
|
||
n = find(props, f"md:{flag_name}")
|
||
if n is not None and inner_text(n) == "true":
|
||
flags.append(flag_label)
|
||
reuse = find(props, "md:ReturnValuesReuse")
|
||
if reuse is not None and inner_text(reuse) != "DontUse":
|
||
reuse_ru = reuse_map.get(inner_text(reuse), inner_text(reuse))
|
||
flags.append(f"Повторное использование: {reuse_ru}")
|
||
if flags:
|
||
out(" | ".join(flags))
|
||
|
||
# ScheduledJob
|
||
if md_type == "ScheduledJob":
|
||
method = find(props, "md:MethodName")
|
||
if method is not None and inner_text(method):
|
||
m_name = inner_text(method)
|
||
m2 = re.match(r'^CommonModule\.(.+)$', m_name)
|
||
if m2:
|
||
m_name = m2.group(1)
|
||
out(f"Метод: {m_name}")
|
||
sj_parts = []
|
||
use = find(props, "md:Use")
|
||
sj_parts.append(f"Использование: {'да' if use is not None and inner_text(use) == 'true' else 'нет'}")
|
||
predef = find(props, "md:Predefined")
|
||
sj_parts.append(f"Предопределённое: {'да' if predef is not None and inner_text(predef) == 'true' else 'нет'}")
|
||
restart_cnt = find(props, "md:RestartCountOnFailure")
|
||
restart_int = find(props, "md:RestartIntervalOnFailure")
|
||
if restart_cnt is not None and inner_text(restart_cnt).isdigit() and int(inner_text(restart_cnt)) > 0:
|
||
sj_parts.append(f"Перезапуск: {inner_text(restart_cnt)} (через {inner_text(restart_int)} сек)")
|
||
out(" | ".join(sj_parts))
|
||
|
||
# EventSubscription
|
||
if md_type == "EventSubscription":
|
||
event = find(props, "md:Event")
|
||
if event is not None and inner_text(event):
|
||
ev_ru = event_map.get(inner_text(event), inner_text(event))
|
||
out(f"Событие: {ev_ru}")
|
||
handler = find(props, "md:Handler")
|
||
if handler is not None and inner_text(handler):
|
||
h_name = inner_text(handler)
|
||
m2 = re.match(r'^CommonModule\.(.+)$', h_name)
|
||
if m2:
|
||
h_name = m2.group(1)
|
||
out(f"Обработчик: {h_name}")
|
||
source = find(props, "md:Source")
|
||
if source is not None:
|
||
src_types = []
|
||
for t in find_all(source, "v8:Type"):
|
||
src_types.append(format_source_type(inner_text(t)))
|
||
if src_types:
|
||
if mode == "full":
|
||
out(f"Источники ({len(src_types)}):")
|
||
for s in src_types:
|
||
out(f" {s}")
|
||
else:
|
||
out(f"Источники ({len(src_types)})")
|
||
|
||
# HTTPService
|
||
if md_type == "HTTPService":
|
||
root_url = find(props, "md:RootURL")
|
||
if root_url is not None and inner_text(root_url):
|
||
out(f"Корневой URL: /{inner_text(root_url)}")
|
||
if child_objs is not None:
|
||
endpoints = get_http_endpoints(child_objs)
|
||
if endpoints:
|
||
out("")
|
||
out(f"Шаблоны URL ({len(endpoints)}):")
|
||
for ep in endpoints:
|
||
out(f" {ep['Template']}")
|
||
for m in ep["Methods"]:
|
||
out(f" {m['HTTPMethod'].ljust(6)} \u2192 {m['Handler']}")
|
||
|
||
# WebService
|
||
if md_type == "WebService":
|
||
ns_url = find(props, "md:Namespace")
|
||
if ns_url is not None and inner_text(ns_url):
|
||
out(f"Пространство имён: {inner_text(ns_url)}")
|
||
if child_objs is not None:
|
||
ops = get_ws_operations(child_objs)
|
||
if ops:
|
||
out("")
|
||
out(f"Операции ({len(ops)}):")
|
||
for op in ops:
|
||
out(f" {op['Name']}({op['Params']}) \u2192 {op['ReturnType']}")
|
||
|
||
# Enum values
|
||
if md_type == "Enum" and child_objs is not None:
|
||
vals = []
|
||
for ev in find_all(child_objs, "md:EnumValue"):
|
||
ep = find(ev, "md:Properties")
|
||
v_name = inner_text(find(ep, "md:Name"))
|
||
v_syn = get_ml_text(find(ep, "md:Synonym"))
|
||
vals.append({"Name": v_name, "Synonym": v_syn})
|
||
if vals:
|
||
out("")
|
||
out(f"Значения ({len(vals)}):")
|
||
ml = get_max_name_len(vals)
|
||
for v in vals:
|
||
padded = v["Name"].ljust(ml)
|
||
syn_text = f'"{v["Synonym"]}"' if v["Synonym"] and v["Synonym"] != v["Name"] else ""
|
||
out(f" {padded} {syn_text}")
|
||
|
||
# Standard attributes
|
||
std_attrs = get_standard_attributes(props, md_type, mode)
|
||
if mode == "full":
|
||
std_attrs += get_standard_attributes_full(props, md_type, obj_name)
|
||
if std_attrs:
|
||
out("")
|
||
out("Стандартные реквизиты:")
|
||
ml = get_max_name_len(std_attrs)
|
||
for s in std_attrs:
|
||
out(format_attr_line(s, ml))
|
||
|
||
# Dimensions (registers)
|
||
if md_type.endswith("Register") and child_objs is not None:
|
||
dims = get_attributes(child_objs, "Dimension", True)
|
||
if dims:
|
||
out("")
|
||
out(f"Измерения ({len(dims)}):")
|
||
ml = get_max_name_len(dims)
|
||
for d in dims:
|
||
out(format_attr_line(d, ml))
|
||
|
||
# Resources (registers)
|
||
if md_type.endswith("Register") and child_objs is not None:
|
||
res = get_attributes(child_objs, "Resource")
|
||
if res:
|
||
out("")
|
||
out(f"Ресурсы ({len(res)}):")
|
||
ml = get_max_name_len(res)
|
||
for r in res:
|
||
out(format_attr_line(r, ml))
|
||
|
||
# Attributes
|
||
if child_objs is not None and md_type != "Enum":
|
||
attrs = get_attributes(child_objs)
|
||
if attrs:
|
||
out("")
|
||
out(f"Реквизиты ({len(attrs)}):")
|
||
sorted_attrs = sort_attrs_ref_first(attrs)
|
||
ml = get_max_name_len(sorted_attrs)
|
||
for a in sorted_attrs:
|
||
out(format_attr_line(a, ml))
|
||
|
||
# Tabular sections
|
||
if child_objs is not None and md_type != "Enum":
|
||
tss = get_tabular_sections(child_objs)
|
||
if tss:
|
||
if mode == "full":
|
||
for ts in tss:
|
||
out("")
|
||
out(f"ТЧ {ts['Name']} ({ts['ColCount']} {decline_cols(ts['ColCount'])}):")
|
||
if ts["ColCount"] > 0:
|
||
sorted_cols = sort_attrs_ref_first(ts["Columns"])
|
||
ml = get_max_name_len(sorted_cols)
|
||
for c in sorted_cols:
|
||
out(format_attr_line(c, ml))
|
||
else:
|
||
out("")
|
||
ts_parts = [f"{t['Name']}({t['ColCount']})" for t in tss]
|
||
out(f"ТЧ ({len(tss)}): {', '.join(ts_parts)}")
|
||
|
||
# Forms/Templates/Commands in overview for Reports & DataProcessors
|
||
if mode == "overview" and child_objs is not None and md_type in ("Report", "DataProcessor"):
|
||
forms = get_simple_children(child_objs, "Form")
|
||
if forms:
|
||
out(f"Формы: {', '.join(forms)}")
|
||
templates = get_simple_children(child_objs, "Template")
|
||
if templates:
|
||
out(f"Макеты: {', '.join(templates)}")
|
||
commands = get_simple_children(child_objs, "Command")
|
||
if commands:
|
||
out(f"Команды: {', '.join(commands)}")
|
||
|
||
# Full mode: additional sections
|
||
if mode == "full" and child_objs is not None:
|
||
# Register records (documents)
|
||
if md_type == "Document":
|
||
reg_recs = []
|
||
for item in find_all(props, "md:RegisterRecords/xr:Item"):
|
||
raw = inner_text(item)
|
||
m2 = re.match(r'^(\w+)\.(.+)$', raw)
|
||
if m2:
|
||
prefix = m2.group(1)
|
||
rname = m2.group(2)
|
||
short = reg_type_map.get(prefix, prefix)
|
||
reg_recs.append(f"{short}.{rname}")
|
||
else:
|
||
reg_recs.append(raw)
|
||
if reg_recs:
|
||
out("")
|
||
out(f"Движения ({len(reg_recs)}): {', '.join(reg_recs)}")
|
||
|
||
# BasedOn
|
||
based_on = []
|
||
for item in find_all(props, "md:BasedOn/xr:Item"):
|
||
raw = inner_text(item)
|
||
m2 = re.match(r'^\w+\.(.+)$', raw)
|
||
if m2:
|
||
based_on.append(m2.group(1))
|
||
else:
|
||
based_on.append(raw)
|
||
if based_on:
|
||
out(f"Ввод на основании: {', '.join(based_on)}")
|
||
|
||
# Forms
|
||
forms = get_simple_children(child_objs, "Form")
|
||
if forms:
|
||
out(f"Формы: {', '.join(forms)}")
|
||
|
||
# Templates
|
||
templates = get_simple_children(child_objs, "Template")
|
||
if templates:
|
||
out(f"Макеты: {', '.join(templates)}")
|
||
|
||
# Commands
|
||
commands = get_simple_children(child_objs, "Command")
|
||
if commands:
|
||
out(f"Команды: {', '.join(commands)}")
|
||
|
||
# Единственная подсказка на весь вывод — чем развернуть свёрнутый в счётчик состав
|
||
if collapsed_names:
|
||
hints = ", ".join(f"-Name {n}" for n in collapsed_names)
|
||
out("")
|
||
out(f"Полный состав типов: {hints}")
|
||
|
||
# ── Pagination and output ────────────────────────────────────
|
||
|
||
total_lines = len(lines)
|
||
out_lines = lines[:]
|
||
|
||
if offset > 0:
|
||
if offset >= total_lines:
|
||
print(f"[INFO] Offset {offset} exceeds total lines ({total_lines}). Nothing to show.")
|
||
sys.exit(0)
|
||
out_lines = out_lines[offset:]
|
||
|
||
if limit > 0 and len(out_lines) > limit:
|
||
shown = out_lines[:limit]
|
||
remaining = total_lines - offset - limit
|
||
shown.append("")
|
||
shown.append(f"[ОБРЕЗАНО] Показано {limit} из {total_lines} строк. Используйте -Offset {offset + limit} для продолжения.")
|
||
out_lines = shown
|
||
|
||
if out_file:
|
||
if not os.path.isabs(out_file):
|
||
out_file = os.path.join(os.getcwd(), out_file)
|
||
with open(out_file, "w", encoding="utf-8-sig") as f:
|
||
f.write("\n".join(out_lines))
|
||
print(f"Output written to {out_file}")
|
||
else:
|
||
for ln in out_lines:
|
||
print(ln)
|