diff --git a/.claude/skills/meta-decompile/SKILL.md b/.claude/skills/meta-decompile/SKILL.md index aa743cfb..49fdf877 100644 --- a/.claude/skills/meta-decompile/SKILL.md +++ b/.claude/skills/meta-decompile/SKILL.md @@ -1,6 +1,6 @@ --- name: meta-decompile -description: Декомпиляция объекта метаданных 1С (Catalog.xml и др.) в JSON-черновик в формате meta-compile. Используй для scaffold нового объекта по образцу. Не для точечных правок +description: Декомпиляция объекта метаданных 1С в JSON-заготовку формата meta-compile. Используй когда нужно получить черновик DSL-описания нового объекта по образцу другого. Не сохраняет UUID/модули/формы. argument-hint: [-OutputPath ] disable-model-invocation: true allowed-tools: @@ -10,18 +10,25 @@ allowed-tools: - Glob --- -# /meta-decompile — JSON-черновик из XML объекта метаданных +# /meta-decompile — DSL-заготовка из XML объекта метаданных -Читает XML объекта метаданных (`Catalogs/Имя.xml` и т.п.) и эмитит компактный JSON в формате `meta-compile`. **Результат — черновик**, а не обратимое представление. +Читает XML объекта метаданных (`Catalogs/Имя.xml` и т.п.) и эмитит компактный JSON в формате `/meta-compile`. Назначение — **взять существующий объект образцом и собрать по нему НОВЫЙ**: декомпилировать → поправить → скомпилировать под другим именем. + +## ⚠️ Главное: это НЕ обратимая выгрузка + +Компиляция черновика создаёт **новый объект с новой идентичностью**, а не копию исходного. В JSON **не** попадают: UUID (идентичность самого объекта и всех дочерних), тела модулей, формы, макеты, права. Захватываются только структура и свойства. + +Отсюда правило: **никогда не компилируй черновик поверх объекта-источника и не выдавай его за «реимпорт»** — у пересобранного объекта другие UUID, поэтому все ссылки на исходный объект (из кода, других объектов, состава подсистем, предопределённых данных) сломаются, а код модулей и формы пропадут. ## Когда использовать -- **Scaffold нового объекта по образцу** — взять существующий объект, получить JSON, поправить и скомпилировать в новый. -- **Структурный обзор** — реквизиты, табличные части, свойства в компактном виде. +**Собрать новый объект по образцу существующего** — получить DSL-заготовку рабочего объекта, переименовать и адаптировать состав, скомпилировать в новый. Быстрее, чем писать DSL с нуля для богатого объекта. ## Когда **не** использовать -- **Точечные правки готового объекта** (добавить реквизит, ТЧ) → `/meta-edit`. Цикл «декомпиляция → правка JSON → компиляция» переписывает объект целиком, может терять непокрытые конструкции. +- **Точечная правка существующего объекта** (добавить реквизит, ТЧ, свойство) → `/meta-edit`. Цикл decompile→compile тут вреден: даёт объект с новой идентичностью и теряет модули/формы. +- **Сохранить / восстановить / перенести тот же объект** (бэкап, миграция между конфигурациями с сохранением ссылок) → штатная выгрузка 1С (`/db-dump-xml` ↔ `/db-load-xml`, CF), а не decompile. +- **Просто понять структуру** объекта (реквизиты, ТЧ, типы) без пересборки → `/meta-info` (дешевле, не плодит файл). ## Параметры @@ -34,15 +41,12 @@ allowed-tools: powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-decompile.ps1" -ObjectPath "<Объект.xml>" -OutputPath "" ``` -## Что получаешь +Неподдерживаемый тип объекта или не-`MetaDataObject` root → ненулевой код выхода и сообщение в stderr. -JSON-черновик в формате `/meta-compile` — **не полное обратимое представление**: раундтрип `xml → json → xml` не гарантируется, часть конструкций DSL не покрывает. +## Workflow (сборка нового объекта по образцу) -Неподдерживаемый тип объекта (на текущем этапе — всё, кроме Catalog) или не-MetaDataObject root → скрипт падает с ненулевым кодом и сообщением в stderr. - -## Workflow - -1. `/meta-decompile <Объект.xml> -OutputPath draft.json` — получить черновик. -2. Поправить JSON под задачу. -3. `/meta-compile -JsonPath draft.json -OutputDir ` — собрать. +1. `/meta-decompile <Образец.xml> -OutputPath draft.json` — получить заготовку. +2. В `draft.json` **сменить `name`** на имя нового объекта и адаптировать состав (реквизиты/ТЧ/свойства). Ссылки на *другие* объекты (владельцы, ввод на основании, типы) — по имени, сохраняются как есть. +3. `/meta-compile -JsonPath draft.json -OutputDir ` — собрать (объект получит свежие UUID). 4. `/meta-validate` + `/meta-info` — проверить. +5. Модули, формы, макеты, права — добавить отдельно (в черновик они не попадают). diff --git a/.claude/skills/meta-decompile/scripts/meta-decompile.py b/.claude/skills/meta-decompile/scripts/meta-decompile.py new file mode 100644 index 00000000..5b465589 --- /dev/null +++ b/.claude/skills/meta-decompile/scripts/meta-decompile.py @@ -0,0 +1,1932 @@ +#!/usr/bin/env python3 +# meta-decompile v0.51 — XML объекта метаданных 1С → JSON-черновик формата meta-compile +# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills +# +# Зеркало meta-decompile.ps1 (КАНОН). Структура 1:1 — те же имена функций, порядок, комментарии. +# Изменения вносить СНАЧАЛА в .ps1, затем переносить сюда. lxml.etree — для полного XPath 1.0 +# (SelectSingleNode/union/предикаты), как XmlNamespaceManager в .ps1. +# +# Поддержаны: Catalog, ExchangePlan, ChartOfCharacteristicTypes, ChartOfAccounts, ChartOfCalculationTypes, Document, +# InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, BusinessProcess, Task, Enum, ... +# Инверс meta-compile (omit-on-default: ключ эмитим только когда значение в XML отличается от умолчания +# компилятора). Неподдерживаемый тип / не-MetaDataObject root → exit 3 (ring3, как form-decompile). + +import argparse +import os +import re +import sys +from lxml import etree + +sys.stdout.reconfigure(encoding="utf-8") +sys.stderr.reconfigure(encoding="utf-8") + +# --- Namespaces (зеркало XmlNamespaceManager) --- +NS_MD = "http://v8.1c.ru/8.3/MDClasses" +NS_V8 = "http://v8.1c.ru/8.1/data/core" +NS_XR = "http://v8.1c.ru/8.3/xcf/readable" +NS_XSI = "http://www.w3.org/2001/XMLSchema-instance" +NS_APP = "http://v8.1c.ru/8.2/managed-application/core" +NS = {'md': NS_MD, 'v8': NS_V8, 'xr': NS_XR, 'xsi': NS_XSI, 'app': NS_APP} + +# Скрипт-скоуп состояние (зеркало $script:* / module-level $props/$dsl/$objType канона). Ставятся в main(). +props = None +obj_node = None +obj_type = None +obj_name = None +dsl = None +object_path = None + + +# --- XML-обёртки (зеркало SelectSingleNode/SelectNodes/GetAttribute/InnerText) --- +def _single(node, xpath): + if node is None: + return None + r = node.xpath(xpath, namespaces=NS) + return r[0] if r else None + + +def _nodes(node, xpath): + if node is None: + return [] + return node.xpath(xpath, namespaces=NS) + + +def _text(node): + """InnerText — конкатенация всего текста-потомка (PreserveWhitespace=true у lxml по умолчанию).""" + if node is None: + return None + return ''.join(node.itertext()) + + +def _attr(node, name, ns=None): + """GetAttribute(name[, ns]) — .NET возвращает '' для отсутствующего атрибута, lxml → None.""" + if node is None: + return '' + key = ('{%s}%s' % (ns, name)) if ns else name + v = node.get(key) + return v if v is not None else '' + + +def _localname(el): + t = el.tag + if not isinstance(t, str): + return '' + return t.split('}', 1)[1] if '}' in t else t + + +# --- JSON-эмиттер (контроль порядка/массивов/кириллицы) --- +def _json_num(n): + """Зеркало "$node" для int/long/double.""" + if isinstance(n, bool): + return 'true' if n else 'false' + if isinstance(n, int): + return str(n) + r = repr(float(n)) + if r.endswith('.0'): + r = r[:-2] + return r + + +def convert_to_compact_json(node, depth=0): + pad = " " * depth + pad1 = " " * (depth + 1) + if node is None: + return "null" + if isinstance(node, bool): + return "true" if node else "false" + if isinstance(node, (int, float)): + return _json_num(node) + if isinstance(node, dict): + if len(node) == 0: + return "{}" + items = [] + for k in node.keys(): + items.append("%s%s: %s" % (pad1, quote_json(str(k)), convert_to_compact_json(node[k], depth + 1))) + return "{\n" + ",\n".join(items) + "\n" + pad + "}" + if isinstance(node, list): + if len(node) == 0: + return "[]" + # Массив скаляров-строк — компактно в строку; массив объектов — по строкам. + all_scalar = True + for e in node: + if isinstance(e, (dict, list)): + all_scalar = False + break + if all_scalar: + items = [convert_to_compact_json(e, depth + 1) for e in node] + return "[" + ", ".join(items) + "]" + items = ["%s%s" % (pad1, convert_to_compact_json(e, depth + 1)) for e in node] + return "[\n" + ",\n".join(items) + "\n" + pad + "]" + return quote_json(str(node)) + + +def quote_json(s): + sb = ['"'] + for ch in s: + if ch == '"': + sb.append('\\"') + elif ch == '\\': + sb.append('\\\\') + elif ch == '\n': + sb.append('\\n') + elif ch == '\r': + sb.append('\\r') + elif ch == '\t': + sb.append('\\t') + else: + if ord(ch) < 32: + sb.append('\\u%04x' % ord(ch)) + else: + sb.append(ch) + sb.append('"') + return ''.join(sb) + + +# --- Сравнение регистров (зеркало PS -ne / -cne) --- +def ne_ci(a, b): + """PS -ne на строках (регистронезависимо). True когда различаются без учёта регистра.""" + return (a if a is not None else '').lower() != (b if b is not None else '').lower() + + +def split_camel_words(name): + """Авто-синоним: точное зеркало Split-CamelCase из meta-compile (split_camel_case). + ВАЖНО: логика должна совпадать байт-в-байт с компилятором, иначе ложные «синоним==авто» → диффы.""" + if not name: + return name + result = re.sub(r'([а-яё])([А-ЯЁ])', r'\1 \2', name) + result = re.sub(r'([a-z])([A-Z])', r'\1 \2', result) + if len(result) > 1: + result = result[0] + result[1:].lower() + return result + + +def P(tag): + n = _single(props, "md:" + tag) + return _text(n) if n is not None else None + + +# --- Synonym (ru) --- +def get_ml_ru(node): + if node is None: + return None + it = _single(node, "v8:item[v8:lang='ru']/v8:content") + if it is not None: + return _text(it) + return None + + +def get_ml_value(node): + """ML-значение → строка (если единственный item ru) ЛИБО dict{lang:content} (мультиязычно, порядок из XML). + None если контента нет. Компактная строка для ru-only, объект для мультиязычных.""" + if node is None: + return None + items = _nodes(node, 'v8:item') + if len(items) == 0: + return None + if len(items) == 1: + lang = _text(_single(items[0], 'v8:lang')) + content = _text(_single(items[0], 'v8:content')) + # Единственный ru-item: пустое содержимое ≡ отсутствие значения → None (иначе tooltip:"" ≠ self-close). + if lang == 'ru': + return None if content == '' else content + o = {} + for it in items: + l = _text(_single(it, 'v8:lang')) + c = _text(_single(it, 'v8:content')) + o[l] = c + return o + + +# --- Тип реквизита: → shorthand-строка --- +def strip_ns_prefix(s): + if ':' in s: + return s.split(':', 1)[1] + return s + + +def get_type_shorthand(type_node): + if type_node is None: + return "" + parts = [] + children = [c for c in type_node if isinstance(c.tag, str)] + for i in range(len(children)): + c = children[i] + ln = _localname(c) + if ln == 'Type': + raw = _text(c).strip() + nxt = children[i + 1] if i + 1 < len(children) else None + if re.search(r'(^|:)boolean$', raw, re.I): + parts.append('Boolean') + elif re.search(r'(^|:)string$', raw, re.I): + length = '10' + al = '' + sq = nxt if (nxt is not None and _localname(nxt) == 'StringQualifiers') else _single(type_node, 'v8:StringQualifiers') + if sq is not None: + l = _single(sq, 'v8:Length') + if l is not None: + length = _text(l) + aln = _single(sq, 'v8:AllowedLength') + if aln is not None and _text(aln) == 'Fixed': + al = ',fixed' + parts.append("String(%s%s)" % (length, al)) + elif re.search(r'(^|:)decimal$', raw, re.I): + d = '10' + f = '0' + sign = '' + nq = nxt if (nxt is not None and _localname(nxt) == 'NumberQualifiers') else _single(type_node, 'v8:NumberQualifiers') + if nq is not None: + dn = _single(nq, 'v8:Digits') + if dn is not None: + d = _text(dn) + fn = _single(nq, 'v8:FractionDigits') + if fn is not None: + f = _text(fn) + sn = _single(nq, 'v8:AllowedSign') + if sn is not None and _text(sn) == 'Nonnegative': + sign = ',nonneg' + parts.append("Number(%s,%s%s)" % (d, f, sign)) + elif re.search(r'(^|:)dateTime$', raw, re.I): + fr = 'DateTime' + dq = nxt if (nxt is not None and _localname(nxt) == 'DateQualifiers') else _single(type_node, 'v8:DateQualifiers') + if dq is not None: + dn = _single(dq, 'v8:DateFractions') + if dn is not None: + fr = _text(dn) + parts.append(fr) # Date | DateTime + elif re.search(r'(^|:)base64Binary$', raw, re.I): + parts.append('ValueStorage') + else: + parts.append(strip_ns_prefix(raw)) # cfg:CatalogRef.X → CatalogRef.X + elif ln == 'TypeSet': + parts.append(strip_ns_prefix(_text(c).strip())) # cfg:DefinedType.X → DefinedType.X + return ' + '.join(parts) + + +# Скалярное значение параметра выбора () → JSON-значение (bool/число/строка). +def convert_ch_scalar_node(vN): + xt = _attr(vN, 'type', NS_XSI) + txt = _text(vN) + if re.search(r'boolean$', xt, re.I): + return txt == 'true' + if re.search(r'decimal$', xt, re.I): + if re.match(r'^-?\d+$', txt): + return int(txt) + return float(txt) + return txt + + +# app:value (тип прямо на узле) → значение ЛИБО список (v8:FixedArray с детьми v8:Value). +def get_choice_param_value(val_node): + xt = _attr(val_node, 'type', NS_XSI) + if re.search(r'FixedArray$', xt, re.I): + arr = [] + for sub in _nodes(val_node, 'v8:Value'): + arr.append(convert_ch_scalar_node(sub)) + return arr + return convert_ch_scalar_node(val_node) + + +# → [{name,dataPath,valueChange?}] | строки "name=dataPath". tag: 'md:...' | 'xr:...'. +def parse_choice_parameter_links(parent, tag): + node = _single(parent, tag) + if node is None: + return None + links = _nodes(node, 'xr:Link') + if len(links) == 0: + return None + arr = [] + for lk in links: + l_name = _text(_single(lk, 'xr:Name')) + l_dp = _text(_single(lk, 'xr:DataPath')) + vc_n = _single(lk, 'xr:ValueChange') + vcv = _text(vc_n) if vc_n is not None else 'Clear' + if vcv == 'Clear': + arr.append("%s=%s" % (l_name, l_dp)) + else: + arr.append({'name': l_name, 'dataPath': l_dp, 'valueChange': vcv}) + # PS-трап: `return $arr` БЕЗ унарной запятой → одноэлементный ArrayList разворачивается в скаляр + # (в отличие от Get-ChoiceParamValue с `,$arr`). Зеркалим: len==1 → голый элемент. + return arr[0] if len(arr) == 1 else arr + + +# → [{name,value?}]. tag: 'md:...' | 'xr:...'. +def parse_choice_parameters(parent, tag): + node = _single(parent, tag) + if node is None: + return None + items = _nodes(node, 'app:item') + if len(items) == 0: + return None + arr = [] + for it in items: + p_name = _attr(it, 'name') + val_n = _single(it, 'app:value') + nil_attr = _attr(val_n, 'nil', NS_XSI) if val_n is not None else '' + if val_n is None or nil_attr == 'true': + arr.append({'name': p_name}) + else: + o = {'name': p_name} + o['value'] = get_choice_param_value(val_n) + arr.append(o) + # PS-трап (как выше): одноэлементный ArrayList разворачивается в скаляр при `return $arr`. + return arr[0] if len(arr) == 1 else arr + + +# --- Реквизит → DSL: shorthand-строка "Имя: Тип | флаги" ЛИБО object-форма при кастомном синониме. --- +def attr_to_dsl(attr_node): + ap = _single(attr_node, 'md:Properties') + nm = _text(_single(ap, 'md:Name')) + ts = get_type_shorthand(_single(ap, 'md:Type')) + flags = [] + fc = _single(ap, 'md:FillChecking') + if fc is not None and _text(fc) == 'ShowError': + flags.append('req') + ix = _single(ap, 'md:Indexing') + if ix is not None: + ixt = _text(ix) + if ixt == 'Index': + flags.append('index') + elif ixt == 'IndexWithAdditionalOrder': + flags.append('indexAdditional') + ml = _single(ap, 'md:MultiLine') + if ml is not None and _text(ml) == 'true': + flags.append('multiline') + + # Синоним/подсказка (строка ru-only ИЛИ {ru,en}). + syn_node = _single(ap, 'md:Synonym') + syn_val = get_ml_value(syn_node) + syn_custom = False + # Пустой (узел есть, значения нет) ≠ авто-синоним из имени → явный пустой (synonym:""). + syn_empty = (syn_node is not None and syn_val is None) + if isinstance(syn_val, str): + if ne_ci(syn_val, split_camel_words(nm)): + syn_custom = True + elif syn_val is not None: + syn_custom = True # {ru,en} = всегда кастом + tt_val = get_ml_value(_single(ap, 'md:ToolTip')) + + # Extra-свойства реквизита (omit-on-default). Наличие любого → object-форма. + def en(tag): + n = _single(ap, "md:" + tag) + return _text(n) if n is not None else None + + extra = {} + v = en('Comment') + if v: + extra['comment'] = v + v = en('FullTextSearch') + if v and v != 'Use': + extra['fullTextSearch'] = v + v = en('FillFromFillingValue') + if v == 'true': + extra['fillFromFillingValue'] = True + v = en('CreateOnInput') + if v and v != 'Auto': + extra['createOnInput'] = v + v = en('QuickChoice') + if v and v != 'Auto': + extra['quickChoice'] = v + v = en('DataHistory') + if v and v != 'Use': + extra['dataHistory'] = v + v = en('Use') + if v and v != 'ForItem': + extra['use'] = v + v = en('PasswordMode') + if v == 'true': + extra['passwordMode'] = True + v = en('Mask') + if v: + extra['mask'] = v + v = en('ChoiceHistoryOnInput') + if v and v != 'Auto': + extra['choiceHistoryOnInput'] = v + v = en('FillChecking') + if v == 'ShowWarning': + extra['fillChecking'] = 'ShowWarning' + v = en('ExtendedEdit') + if v == 'true': + extra['extendedEdit'] = True + v = en('MarkNegatives') + if v == 'true': + extra['markNegatives'] = True + v = en('ChoiceFoldersAndItems') + if v and v != 'Items': + extra['choiceFoldersAndItems'] = v + v = en('ChoiceForm') + if v: + extra['choiceForm'] = v + # Регистро-специфика измерения (теги присутствуют только у Dimension → безвредно для прочих). + v = en('Master') + if v == 'true': + extra['master'] = True + v = en('MainFilter') + if v == 'true': + extra['mainFilter'] = True + v = en('DenyIncompleteValues') + if v == 'true': + extra['denyIncompleteValues'] = True + v = en('UseInTotals') + if v == 'false': + extra['useInTotals'] = False # дефолт true → захват при false + v = en('BaseDimension') + if v == 'true': + extra['baseDimension'] = True + v = en('ScheduleLink') + if v: + extra['scheduleLink'] = v # ссылка на измерение графика (пустой → пропуск) + v = en('Balance') + if v == 'true': + extra['balance'] = True + v = en('AccountingFlag') + if v: + extra['accountingFlag'] = v # ссылка на признак учёта ПС (пустой → пропуск) + v = en('ExtDimensionAccountingFlag') + if v: + extra['extDimensionAccountingFlag'] = v + v = en('AddressingDimension') + if v: + extra['addressingDimension'] = v # ссылка на измерение регистра исполнителей + # MinValue/MaxValue — граница диапазона (omit при nil). Тип сохраняем: xs:string→строка, xs:decimal→число. + for mm in ('MinValue', 'MaxValue'): + mn = _single(ap, "md:" + mm) + if mn is None: + continue + if _attr(mn, 'nil', NS_XSI) == 'true': + continue + key = 'minValue' if mm == 'MinValue' else 'maxValue' + xt = _attr(mn, 'type', NS_XSI) + txt = _text(mn) + if re.search(r'decimal|int|double|float', xt, re.I): + extra[key] = int(txt) if re.match(r'^-?\d+$', txt) else float(txt) + else: + extra[key] = txt + fmt_v = get_ml_value(_single(ap, 'md:Format')) + if fmt_v is not None: + extra['format'] = fmt_v + efmt_v = get_ml_value(_single(ap, 'md:EditFormat')) + if efmt_v is not None: + extra['editFormat'] = efmt_v + + # FillValue (значение заполнения). Форма по умолчанию зависит от типа реквизита: String→typed-empty, + # Number→zero, всё остальное→nil. Эмитим `fillValue` только при отклонении от дефолта (§4.2 spec). + fv_node = _single(ap, 'md:FillValue') + if fv_node is not None: + fcat = 'Other' + if re.search(r'\+', ts): + fcat = 'Other' + elif re.search(r'^Boolean', ts): + fcat = 'Boolean' + elif re.search(r'^String', ts): + fcat = 'String' + elif re.search(r'^Number', ts): + fcat = 'Number' + elif re.search(r'^(Date|DateTime)', ts): + fcat = 'Date' + nil_attr = _attr(fv_node, 'nil', NS_XSI) + xsi_t = _attr(fv_node, 'type', NS_XSI) + fv_text = _text(fv_node) + if nil_attr == 'true': + if fcat == 'String' or fcat == 'Number': + extra['fillValue'] = None # nil-override + # иначе nil — это дефолт → пропускаем + elif re.search(r'boolean$', xsi_t, re.I): + extra['fillValue'] = (fv_text == 'true') + elif re.search(r'decimal$', xsi_t, re.I): + # Захватываем как ЧИСЛО (не строку): на составном типе компилятор берёт xsi-тип из JSON-значения. + if not (fcat == 'Number' and (fv_text == '0' or fv_text == '')): + extra['fillValue'] = int(fv_text) if re.match(r'^-?\d+$', fv_text) else float(fv_text) + elif re.search(r'string$', xsi_t, re.I): + if not (fcat == 'String' and fv_text == ''): + extra['fillValue'] = fv_text + elif re.search(r'dateTime$', xsi_t, re.I): + extra['fillValue'] = fv_text + elif re.search(r'DesignTimeRef$', xsi_t, re.I): + # Пустой DTR ≠ nil/xs:string → маркер emptyRef (иначе тип терялся в xs:string). + if fv_text == '': + extra['fillValue'] = {'emptyRef': True} + else: + extra['fillValue'] = fv_text + + # LinkByType (связь по типу): DataPath + LinkItem. Пусто → пропускаем. linkItem=0 → компактно строкой. + lbt_node = _single(ap, 'md:LinkByType') + if lbt_node is not None: + dp_n = _single(lbt_node, 'xr:DataPath') + if dp_n is not None and _text(dp_n): + li_n = _single(lbt_node, 'xr:LinkItem') + li = int(_text(li_n)) if (li_n is not None and _text(li_n)) else 0 + if li == 0: + extra['linkByType'] = _text(dp_n) + else: + extra['linkByType'] = {'dataPath': _text(dp_n), 'linkItem': li} + + cpl_arr = parse_choice_parameter_links(ap, 'md:ChoiceParameterLinks') + if cpl_arr is not None: + extra['choiceParameterLinks'] = cpl_arr + cp_arr = parse_choice_parameters(ap, 'md:ChoiceParameters') + if cp_arr is not None: + extra['choiceParameters'] = cp_arr + + # Пустой (реквизит без типа) → ts=''. Отличаем от «дефолтного» отсутствия: явный type:''. + type_empty = (ts == '') + if syn_custom or syn_empty or (tt_val is not None) or len(extra) > 0 or type_empty: + o = {'name': nm} + if ts: + o['type'] = ts + elif type_empty: + o['type'] = '' + if syn_custom: + o['synonym'] = syn_val + elif syn_empty: + o['synonym'] = '' + if tt_val is not None: + o['tooltip'] = tt_val + for k in extra: + o[k] = extra[k] + if len(flags) > 0: + o['flags'] = list(flags) + return o + head = ("%s: %s" % (nm, ts)) if ts else nm + if len(flags) > 0: + return head + " | " + ", ".join(flags) + return head + + +# --- Свойства (omit-on-default). --- +def add_bool_prop(key, tag, default): + v = P(tag) + if v is not None: + b = (v == 'true') + if b != default: + dsl[key] = b + + +def add_enum_prop(key, tag, default): + v = P(tag) + if v is not None and v != '' and v != default: + dsl[key] = v + + +def add_int_prop(key, tag, default): + v = P(tag) + if v is not None and v != '': + iv = int(v) + if iv != default: + dsl[key] = iv + + +# Общий захват структурного (зеркало Emit-CommandPicture). Пишет в tgt ключи picture/loadTransparent. +def get_picture_to_dsl(props_node, tgt): + ref_n = _single(props_node, 'md:Picture/xr:Ref') + abs_n = _single(props_node, 'md:Picture/xr:Abs') + if ref_n is not None or abs_n is not None: + psrc = _text(ref_n) if ref_n is not None else ("abs:" + _text(abs_n)) + lt_n = _single(props_node, 'md:Picture/xr:LoadTransparent') + lt_false = (lt_n is not None and _text(lt_n) == 'false') + tpx_n = _single(props_node, 'md:Picture/xr:TransparentPixel') + if tpx_n is not None: + po = {'src': psrc} + if lt_false: + po['loadTransparent'] = False + po['transparentPixel'] = {'x': int(_attr(tpx_n, 'x')), 'y': int(_attr(tpx_n, 'y'))} + tgt['picture'] = po + else: + tgt['picture'] = psrc + if lt_false: + tgt['loadTransparent'] = False + + +# Короткая форма поля: ..StandardAttribute.X / .Attribute.X → StandardAttribute.X / Attribute.X +def short_field(full): + m = re.search(r'\.(StandardAttribute|Attribute)\.(.+)$', full) + if m: + return "%s.%s" % (m.group(1), m.group(2)) + return full + + +def add_form_ref(key, tag): + v = P(tag) + if v: + dsl[key] = v + + +# --- Characteristics: короткая форма поля bare/partial. --- +def shorten_char_field(full, frm): + if full.startswith(frm + "."): + rest = full[len(frm) + 1:] + m = re.match(r'^StandardAttribute\.(Ref|Parent|Owner)$', rest) + if m: + return m.group(1) # ссылочные станд. → голое + m = re.match(r'^Attribute\.(.+)$', rest) + if m: + return m.group(1) # кастом → голое + return rest # прочие StandardAttribute.X / Dimension.X / Resource.X → частичное + return full + + +# --- Предопределённые (local-name xpath, без ns-менеджера) --- +def _lx1(el, xp): + r = el.xpath(xp) + return r[0] if r else None + + +def _lxn(el, xp): + return el.xpath(xp) + + +def predef_item_to_dsl(item_el): + name = _text(_lx1(item_el, "*[local-name()='Name']")) + code_el = _lx1(item_el, "*[local-name()='Code']") + code = _text(code_el) if (code_el is not None and _text(code_el)) else '' + desc_el = _lx1(item_el, "*[local-name()='Description']") + desc = _text(desc_el) if desc_el is not None else '' + folder_el = _lx1(item_el, "*[local-name()='IsFolder']") + is_folder = (folder_el is not None and _text(folder_el) == 'true') + child_container = _lx1(item_el, "*[local-name()='ChildItems']") + kids = _lxn(child_container, "*[local-name()='Item']") if child_container is not None else [] + # Type — тип значения предопределённой характеристики (ПВХ). Наличие узла → object-форма с ключом type. + type_el = _lx1(item_el, "*[local-name()='Type']") + type_str = get_type_shorthand(type_el) if type_el is not None else None + auto = split_camel_words(name) + + # Компактная строка для плоских: без узла Type (Catalog) ИЛИ с непустым типом → "(Код) Имя [Наим]: Тип". + if (not is_folder) and len(kids) == 0 and (type_str is None or type_str != ''): + s = ("(%s) %s" % (code, name)) if code else name + if desc == '': + s = s + " []" + elif ne_ci(desc, auto): + s = s + (" [%s]" % desc) + if type_str: + s = "%s: %s" % (s, type_str) + return s + # Группа/иерархия/с типом → объект. + o = {'name': name} + if code: + o['code'] = code + if desc == '': + o['description'] = '' + elif ne_ci(desc, auto): + o['description'] = desc + if type_str is not None: + o['type'] = type_str + if is_folder: + o['isFolder'] = True + if len(kids) > 0: + sub = [] + for k in kids: + sub.append(predef_item_to_dsl(k)) + o['childItems'] = sub + return o + + +def predef_account_to_dsl(item_el): + name = _text(_lx1(item_el, "*[local-name()='Name']")) + code_el = _lx1(item_el, "*[local-name()='Code']") + code = _text(code_el) if (code_el is not None and _text(code_el)) else '' + desc_el = _lx1(item_el, "*[local-name()='Description']") + desc = _text(desc_el) if desc_el is not None else '' + at_el = _lx1(item_el, "*[local-name()='AccountType']") + acct_type = _text(at_el) if at_el is not None else 'ActivePassive' + off_el = _lx1(item_el, "*[local-name()='OffBalance']") + off = (off_el is not None and _text(off_el) == 'true') + ord_el = _lx1(item_el, "*[local-name()='Order']") + order = _text(ord_el) if ord_el is not None else '' + auto = split_camel_words(name) + # TRUE-флаги (leaf после последней точки в ref). + true_flags = [] + for fl in _lxn(item_el, "*[local-name()='AccountingFlags']/*[local-name()='Flag']"): + if _text(fl) == 'true': + r = _attr(fl, 'ref') + true_flags.append(r.split('.')[-1]) + # ExtDimensionTypes → subconto. Короткая запись "Тип | Признак1, Признак2". + edt_pfx = (dsl['extDimensionTypes'] + ".") if dsl.get('extDimensionTypes') else None + subconto = [] + for edt in _lxn(item_el, "*[local-name()='ExtDimensionTypes']/*[local-name()='ExtDimensionType']"): + sc_t = _attr(edt, 'name') + if edt_pfx and sc_t.startswith(edt_pfx): + sc_t = sc_t[len(edt_pfx):] + t_el = _lx1(edt, "*[local-name()='Turnover']") + sc_flags_out = [] + if t_el is not None and _text(t_el) == 'true': + sc_flags_out.append('Turnover') + for fl in _lxn(edt, "*[local-name()='AccountingFlags']/*[local-name()='Flag']"): + if _text(fl) == 'true': + r = _attr(fl, 'ref') + sc_flags_out.append(r.split('.')[-1]) + if len(sc_flags_out) > 0: + subconto.append(sc_t + " | " + ", ".join(sc_flags_out)) + else: + subconto.append(sc_t) + child_container = _lx1(item_el, "*[local-name()='ChildItems']") + kids = _lxn(child_container, "*[local-name()='Item']") if child_container is not None else [] + + o = {'name': name} + if code: + o['code'] = code + # -cne (регистрочувствительно!): хвостовые аббревиатуры (ОС/НМА) теряли бы регистр при -ne. + if desc != auto: + o['description'] = desc + o['accountType'] = acct_type + if off: + o['offBalance'] = True + o['order'] = order + if len(true_flags) > 0: + o['flags'] = list(true_flags) + if len(subconto) > 0: + o['subconto'] = subconto + if len(kids) > 0: + sub = [] + for k in kids: + sub.append(predef_account_to_dsl(k)) + o['childItems'] = sub + return o + + +def predef_calc_type_to_dsl(item_el): + name = _text(_lx1(item_el, "*[local-name()='Name']")) + code_el = _lx1(item_el, "*[local-name()='Code']") + code = _text(code_el) if (code_el is not None and _text(code_el)) else '' + desc_el = _lx1(item_el, "*[local-name()='Description']") + desc = _text(desc_el) if desc_el is not None else '' + apib_el = _lx1(item_el, "*[local-name()='ActionPeriodIsBase']") + apib = (apib_el is not None and _text(apib_el) == 'true') + auto = split_camel_words(name) + if not apib: + s = ("(%s) %s" % (code, name)) if code else name + if desc == '': + s = s + " []" + elif desc != auto: # -cne + s = s + (" [%s]" % desc) + return s + o = {'name': name} + if code: + o['code'] = code + if desc != auto: # -cne + o['description'] = desc + o['actionPeriodIsBase'] = True + return o + + +def build_dsl(): + """Линейный поток сборки DSL — зеркало script-body .ps1 (стр.392–1459).""" + global dsl + + # === Сборка DSL === + dsl = {'type': obj_type, 'name': obj_name} + + # Синоним объекта: строка ru-only ИЛИ {ru,en}. Кастом → эмитим. Пустой → явный synonym:"". + syn_node_obj = _single(props, 'md:Synonym') + syn_val = get_ml_value(syn_node_obj) + if isinstance(syn_val, str): + if ne_ci(syn_val, split_camel_words(obj_name)): + dsl['synonym'] = syn_val + elif syn_val is not None: + dsl['synonym'] = syn_val + elif syn_node_obj is not None: + dsl['synonym'] = '' + cmt = P('Comment') + if cmt: + dsl['comment'] = cmt + + # Свойства Catalog (omit-on-default). + add_bool_prop('hierarchical', 'Hierarchical', False) + add_enum_prop('hierarchyType', 'HierarchyType', 'HierarchyFoldersAndItems') + add_bool_prop('limitLevelCount', 'LimitLevelCount', False) + add_int_prop('levelCount', 'LevelCount', 2) + add_bool_prop('foldersOnTop', 'FoldersOnTop', True) + # owners + owners_node = _single(props, 'md:Owners') + if owners_node is not None: + items = [_text(x) for x in _nodes(owners_node, 'xr:Item')] + if len(items) > 0: + dsl['owners'] = [(s.split('.', 1)[1] if re.match(r'^Catalog\.', s) else s) for s in items] + add_enum_prop('subordinationUse', 'SubordinationUse', 'ToItems') + # Тип-зависимые дефолты (компилятор задаёт их по типу — декомпилятор обязан зеркалить). + descr_len_def = {'ExchangePlan': 150, 'ChartOfCharacteristicTypes': 100, 'ChartOfCalculationTypes': 100}.get(obj_type, 25) + code_len_def = 5 if obj_type == 'ChartOfCalculationTypes' else 9 + create_inp_def = 'Use' if obj_type in ('Catalog', 'Document') else 'DontUse' + data_lock_def = 'Automatic' if obj_type in ('Catalog', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Sequence') else 'Managed' + code_series_def = {'ChartOfCharacteristicTypes': 'WholeCharacteristicKind', 'ChartOfAccounts': 'WholeChartOfAccounts'}.get(obj_type, 'WholeCatalog') + check_unique_def = (obj_type in ('ChartOfCharacteristicTypes', 'ChartOfAccounts', 'Document', 'DocumentNumerator')) + def_pres_def = 'AsCode' if obj_type == 'ChartOfAccounts' else 'AsDescription' + add_int_prop('codeLength', 'CodeLength', code_len_def) + add_int_prop('descriptionLength', 'DescriptionLength', descr_len_def) + add_enum_prop('codeType', 'CodeType', 'String') + add_enum_prop('codeAllowedLength', 'CodeAllowedLength', 'Variable') + add_bool_prop('autonumbering', 'Autonumbering', True) + add_bool_prop('checkUnique', 'CheckUnique', check_unique_def) + add_enum_prop('codeSeries', 'CodeSeries', code_series_def) + add_enum_prop('defaultPresentation', 'DefaultPresentation', def_pres_def) + if obj_type != 'Constant': + add_bool_prop('quickChoice', 'QuickChoice', True if obj_type == 'Enum' else False) + add_enum_prop('choiceMode', 'ChoiceMode', 'BothWays') + add_enum_prop('dataLockControlMode', 'DataLockControlMode', data_lock_def) + add_enum_prop('fullTextSearch', 'FullTextSearch', 'Use') + add_bool_prop('useStandardCommands', 'UseStandardCommands', False if obj_type in ('Enum', 'CommonForm') else True) + add_enum_prop('createOnInput', 'CreateOnInput', create_inp_def) + add_enum_prop('editType', 'EditType', 'InDialog') + add_bool_prop('includeHelpInContents', 'IncludeHelpInContents', False) + add_enum_prop('choiceHistoryOnInput', 'ChoiceHistoryOnInput', 'Auto') + add_enum_prop('predefinedDataUpdate', 'PredefinedDataUpdate', 'Auto') + add_enum_prop('searchStringModeOnInputByString', 'SearchStringModeOnInputByString', 'Begin') + add_enum_prop('fullTextSearchOnInputByString', 'FullTextSearchOnInputByString', 'DontUse') + # ExchangePlan-специфичные свойства. + if obj_type == 'ExchangePlan': + add_bool_prop('distributedInfoBase', 'DistributedInfoBase', False) + add_bool_prop('includeConfigurationExtensions', 'IncludeConfigurationExtensions', False) + add_enum_prop('dataHistory', 'DataHistory', 'DontUse') + add_bool_prop('updateDataHistoryImmediatelyAfterWrite', 'UpdateDataHistoryImmediatelyAfterWrite', False) + add_bool_prop('executeAfterWriteDataHistoryVersionProcessing', 'ExecuteAfterWriteDataHistoryVersionProcessing', False) + # ChartOfCharacteristicTypes-специфичные свойства. + if obj_type == 'ChartOfCharacteristicTypes': + add_enum_prop('dataHistory', 'DataHistory', 'DontUse') + add_bool_prop('updateDataHistoryImmediatelyAfterWrite', 'UpdateDataHistoryImmediatelyAfterWrite', False) + add_bool_prop('executeAfterWriteDataHistoryVersionProcessing', 'ExecuteAfterWriteDataHistoryVersionProcessing', False) + cev = P('CharacteristicExtValues') + if cev: + dsl['characteristicExtValues'] = cev + vt_node = _single(props, 'md:Type') + if vt_node is not None: + vt_str = get_type_shorthand(vt_node) + if vt_str and vt_str != 'Boolean + String(100) + Number(15,2) + DateTime': + dsl['valueType'] = vt_str + # ChartOfAccounts-специфичные свойства. + if obj_type == 'ChartOfAccounts': + edt = P('ExtDimensionTypes') + if edt: + dsl['extDimensionTypes'] = edt + add_int_prop('maxExtDimensionCount', 'MaxExtDimensionCount', 3 if edt else 0) + cm = P('CodeMask') + if cm: + dsl['codeMask'] = cm + add_bool_prop('autoOrderByCode', 'AutoOrderByCode', True) + add_int_prop('orderLength', 'OrderLength', 9) + add_enum_prop('dataHistory', 'DataHistory', 'DontUse') + add_bool_prop('updateDataHistoryImmediatelyAfterWrite', 'UpdateDataHistoryImmediatelyAfterWrite', False) + add_bool_prop('executeAfterWriteDataHistoryVersionProcessing', 'ExecuteAfterWriteDataHistoryVersionProcessing', False) + # ChartOfCalculationTypes-специфичные свойства. + if obj_type == 'ChartOfCalculationTypes': + add_enum_prop('dependenceOnCalculationTypes', 'DependenceOnCalculationTypes', 'DontUse') + add_bool_prop('actionPeriodUse', 'ActionPeriodUse', False) + bct_node = _single(props, 'md:BaseCalculationTypes') + if bct_node is not None: + bct_items = [_text(x) for x in _nodes(bct_node, 'xr:Item')] + if len(bct_items) > 0: + dsl['baseCalculationTypes'] = list(bct_items) + add_enum_prop('dataHistory', 'DataHistory', 'DontUse') + add_bool_prop('updateDataHistoryImmediatelyAfterWrite', 'UpdateDataHistoryImmediatelyAfterWrite', False) + add_bool_prop('executeAfterWriteDataHistoryVersionProcessing', 'ExecuteAfterWriteDataHistoryVersionProcessing', False) + # Document-специфичные свойства. + if obj_type == 'Document': + num_ref = P('Numerator') + if num_ref: + dsl['numerator'] = num_ref + add_enum_prop('numberType', 'NumberType', 'String') + add_int_prop('numberLength', 'NumberLength', 11) + add_enum_prop('numberAllowedLength', 'NumberAllowedLength', 'Variable') + add_enum_prop('numberPeriodicity', 'NumberPeriodicity', 'Year') + add_enum_prop('posting', 'Posting', 'Allow') + add_enum_prop('realTimePosting', 'RealTimePosting', 'Deny') + add_enum_prop('registerRecordsDeletion', 'RegisterRecordsDeletion', 'AutoDelete') + add_enum_prop('registerRecordsWritingOnPost', 'RegisterRecordsWritingOnPost', 'WriteSelected') + add_enum_prop('sequenceFilling', 'SequenceFilling', 'AutoFill') + add_bool_prop('postInPrivilegedMode', 'PostInPrivilegedMode', True) + add_bool_prop('unpostInPrivilegedMode', 'UnpostInPrivilegedMode', True) + rr_node = _single(props, 'md:RegisterRecords') + if rr_node is not None: + rr_items = [_text(x) for x in _nodes(rr_node, 'xr:Item')] + if len(rr_items) > 0: + dsl['registerRecords'] = list(rr_items) + add_enum_prop('dataHistory', 'DataHistory', 'DontUse') + add_bool_prop('updateDataHistoryImmediatelyAfterWrite', 'UpdateDataHistoryImmediatelyAfterWrite', False) + add_bool_prop('executeAfterWriteDataHistoryVersionProcessing', 'ExecuteAfterWriteDataHistoryVersionProcessing', False) + # InformationRegister-специфичные свойства. + if obj_type == 'InformationRegister': + add_enum_prop('periodicity', 'InformationRegisterPeriodicity', 'Nonperiodical') + add_enum_prop('writeMode', 'WriteMode', 'Independent') + add_bool_prop('mainFilterOnPeriod', 'MainFilterOnPeriod', False) + add_bool_prop('enableTotalsSliceFirst', 'EnableTotalsSliceFirst', False) + add_bool_prop('enableTotalsSliceLast', 'EnableTotalsSliceLast', False) + add_enum_prop('dataHistory', 'DataHistory', 'DontUse') + add_bool_prop('updateDataHistoryImmediatelyAfterWrite', 'UpdateDataHistoryImmediatelyAfterWrite', False) + add_bool_prop('executeAfterWriteDataHistoryVersionProcessing', 'ExecuteAfterWriteDataHistoryVersionProcessing', False) + # AccumulationRegister-специфичные свойства. + if obj_type == 'AccumulationRegister': + add_enum_prop('registerType', 'RegisterType', 'Balance') + add_bool_prop('enableTotalsSplitting', 'EnableTotalsSplitting', True) + # AccountingRegister-специфичные свойства. + if obj_type == 'AccountingRegister': + coa = P('ChartOfAccounts') + if coa: + dsl['chartOfAccounts'] = coa + add_bool_prop('correspondence', 'Correspondence', False) + add_int_prop('periodAdjustmentLength', 'PeriodAdjustmentLength', 0) + add_bool_prop('enableTotalsSplitting', 'EnableTotalsSplitting', True) + # CalculationRegister-специфичные свойства. + if obj_type == 'CalculationRegister': + cct = P('ChartOfCalculationTypes') + if cct: + dsl['chartOfCalculationTypes'] = cct + add_enum_prop('periodicity', 'Periodicity', 'Month') + add_bool_prop('actionPeriod', 'ActionPeriod', False) + add_bool_prop('basePeriod', 'BasePeriod', False) + sch = P('Schedule') + if sch: + dsl['schedule'] = sch + schv = P('ScheduleValue') + if schv: + dsl['scheduleValue'] = schv + schd = P('ScheduleDate') + if schd: + dsl['scheduleDate'] = schd + # BusinessProcess-специфичные свойства. + if obj_type == 'BusinessProcess': + add_enum_prop('numberType', 'NumberType', 'String') + add_int_prop('numberLength', 'NumberLength', 11) + add_enum_prop('numberAllowedLength', 'NumberAllowedLength', 'Variable') + add_enum_prop('numberPeriodicity', 'NumberPeriodicity', 'Nonperiodical') + add_bool_prop('checkUnique', 'CheckUnique', True) + add_bool_prop('autonumbering', 'Autonumbering', True) + tsk = P('Task') + if tsk: + dsl['task'] = tsk + add_bool_prop('createTaskInPrivilegedMode', 'CreateTaskInPrivilegedMode', True) + # Task-специфичные свойства. + if obj_type == 'Task': + add_enum_prop('numberType', 'NumberType', 'String') + add_int_prop('numberLength', 'NumberLength', 14) + add_enum_prop('numberAllowedLength', 'NumberAllowedLength', 'Variable') + add_bool_prop('checkUnique', 'CheckUnique', True) + add_bool_prop('autonumbering', 'Autonumbering', True) + tnap = P('TaskNumberAutoPrefix') + if tnap and tnap != 'BusinessProcessNumber': + dsl['taskNumberAutoPrefix'] = tnap + add_int_prop('descriptionLength', 'DescriptionLength', 150) + addr = P('Addressing') + if addr: + dsl['addressing'] = addr + maa = P('MainAddressingAttribute') + if maa: + dsl['mainAddressingAttribute'] = maa + cp = P('CurrentPerformer') + if cp: + dsl['currentPerformer'] = cp + add_enum_prop('defaultPresentation', 'DefaultPresentation', 'AsDescription') + # Report-специфичные свойства. + if obj_type == 'Report': + dfm = P('DefaultForm') + if dfm: + dsl['defaultForm'] = dfm + afm = P('AuxiliaryForm') + if afm: + dsl['auxiliaryForm'] = afm + mdcs = P('MainDataCompositionSchema') + if mdcs: + dsl['mainDataCompositionSchema'] = mdcs + dsf = P('DefaultSettingsForm') + if dsf: + dsl['defaultSettingsForm'] = dsf + asf = P('AuxiliarySettingsForm') + if asf: + dsl['auxiliarySettingsForm'] = asf + dvf = P('DefaultVariantForm') + if dvf: + dsl['defaultVariantForm'] = dvf + vs = P('VariantsStorage') + if vs: + dsl['variantsStorage'] = vs + ss = P('SettingsStorage') + if ss: + dsl['settingsStorage'] = ss + ep = get_ml_value(_single(props, 'md:ExtendedPresentation')) + if ep is not None: + dsl['extendedPresentation'] = ep + # DataProcessor-специфичные свойства. + if obj_type == 'DataProcessor': + dfm = P('DefaultForm') + if dfm: + dsl['defaultForm'] = dfm + afm = P('AuxiliaryForm') + if afm: + dsl['auxiliaryForm'] = afm + ep = get_ml_value(_single(props, 'md:ExtendedPresentation')) + if ep is not None: + dsl['extendedPresentation'] = ep + # DefinedType — тип-псевдоним. + if obj_type == 'DefinedType': + vt = get_type_shorthand(_single(props, 'md:Type')) + if vt: + dsl['valueType'] = vt + # FunctionalOption. + if obj_type == 'FunctionalOption': + loc = P('Location') + if loc: + dsl['location'] = loc + add_bool_prop('privilegedGetMode', 'PrivilegedGetMode', True) + content_node = _single(props, 'md:Content') + if content_node is not None: + items = [_text(x) for x in _nodes(content_node, 'xr:Object')] + if len(items) > 0: + dsl['content'] = list(items) + # DocumentJournal. + if obj_type == 'DocumentJournal': + dfm = P('DefaultForm') + if dfm: + dsl['defaultForm'] = dfm + afm = P('AuxiliaryForm') + if afm: + dsl['auxiliaryForm'] = afm + rd_node = _single(props, 'md:RegisteredDocuments') + if rd_node is not None: + rd_items = [_text(x) for x in _nodes(rd_node, 'xr:Item')] + if len(rd_items) > 0: + dsl['registeredDocuments'] = list(rd_items) + # Sequence. + if obj_type == 'Sequence': + add_enum_prop('moveBoundaryOnPosting', 'MoveBoundaryOnPosting', 'DontMove') + for ll in (('Documents', 'documents'), ('RegisterRecords', 'registerRecords')): + ln = _single(props, "md:" + ll[0]) + if ln is not None: + items = [_text(x) for x in _nodes(ln, 'xr:Item')] + if len(items) > 0: + dsl[ll[1]] = list(items) + # FilterCriterion. + if obj_type == 'FilterCriterion': + vt = get_type_shorthand(_single(props, 'md:Type')) + if vt: + dsl['valueType'] = vt + cn = _single(props, 'md:Content') + if cn is not None: + items = [_text(x) for x in _nodes(cn, 'xr:Item')] + if len(items) > 0: + dsl['content'] = list(items) + dfm = P('DefaultForm') + if dfm: + dsl['defaultForm'] = dfm + afm = P('AuxiliaryForm') + if afm: + dsl['auxiliaryForm'] = afm + # DocumentNumerator. + if obj_type == 'DocumentNumerator': + add_enum_prop('numberType', 'NumberType', 'String') + add_int_prop('numberLength', 'NumberLength', 11) + add_enum_prop('numberAllowedLength', 'NumberAllowedLength', 'Variable') + add_enum_prop('numberPeriodicity', 'NumberPeriodicity', 'Year') + # SettingsStorage. + if obj_type == 'SettingsStorage': + for fp in (('DefaultSaveForm', 'defaultSaveForm'), ('DefaultLoadForm', 'defaultLoadForm'), + ('AuxiliarySaveForm', 'auxiliarySaveForm'), ('AuxiliaryLoadForm', 'auxiliaryLoadForm')): + fv = P(fp[0]) + if fv: + dsl[fp[1]] = fv + # CommonModule. + if obj_type == 'CommonModule': + add_bool_prop('global', 'Global', False) + add_bool_prop('clientManagedApplication', 'ClientManagedApplication', False) + add_bool_prop('server', 'Server', False) + add_bool_prop('externalConnection', 'ExternalConnection', False) + add_bool_prop('clientOrdinaryApplication', 'ClientOrdinaryApplication', False) + add_bool_prop('serverCall', 'ServerCall', False) + add_bool_prop('privileged', 'Privileged', False) + add_enum_prop('returnValuesReuse', 'ReturnValuesReuse', 'DontUse') + # EventSubscription. + if obj_type == 'EventSubscription': + src_node = _single(props, 'md:Source') + if src_node is not None: + src_types = [strip_ns_prefix(_text(x).strip()) for x in _nodes(src_node, 'v8:Type|v8:TypeSet')] + if len(src_types) > 0: + dsl['source'] = list(src_types) + add_enum_prop('event', 'Event', 'BeforeWrite') + h = P('Handler') + if h: + dsl['handler'] = h + # CommonForm. + if obj_type == 'CommonForm': + add_enum_prop('formType', 'FormType', 'Managed') + up_node = _single(props, 'md:UsePurposes') + if up_node is not None: + ups = [_text(x) for x in _nodes(up_node, 'v8:Value')] + def2 = ['PlatformApplication', 'MobilePlatformApplication'] + same = (len(ups) == len(def2)) + if same: + for k in range(len(ups)): + if ups[k] != def2[k]: + same = False + break + if (not same) and len(ups) > 0: + dsl['usePurposes'] = list(ups) + ep = get_ml_value(_single(props, 'md:ExtendedPresentation')) + if ep is not None: + dsl['extendedPresentation'] = ep + # SessionParameter. + if obj_type == 'SessionParameter': + vt = get_type_shorthand(_single(props, 'md:Type')) + if vt: + dsl['valueType'] = vt + # FunctionalOptionsParameter. + if obj_type == 'FunctionalOptionsParameter': + un = _single(props, 'md:Use') + if un is not None: + items = [_text(x) for x in _nodes(un, 'xr:Item')] + if len(items) > 0: + dsl['use'] = list(items) + # WSReference. + if obj_type == 'WSReference': + url = P('LocationURL') + if url: + dsl['locationURL'] = url + # CommonPicture. + if obj_type == 'CommonPicture': + add_bool_prop('availabilityForChoice', 'AvailabilityForChoice', False) + add_bool_prop('availabilityForAppearance', 'AvailabilityForAppearance', False) + # CommonTemplate. + if obj_type == 'CommonTemplate': + add_enum_prop('templateType', 'TemplateType', 'SpreadsheetDocument') + # CommandGroup. + if obj_type == 'CommandGroup': + add_enum_prop('representation', 'Representation', 'Auto') + tt = get_ml_value(_single(props, 'md:ToolTip')) + if tt is not None: + dsl['tooltip'] = tt + get_picture_to_dsl(props, dsl) + add_enum_prop('category', 'Category', 'NavigationPanel') + # CommonCommand. + if obj_type == 'CommonCommand': + grp = P('Group') + if grp: + dsl['group'] = grp + add_enum_prop('representation', 'Representation', 'Auto') + tt = get_ml_value(_single(props, 'md:ToolTip')) + if tt is not None: + dsl['tooltip'] = tt + get_picture_to_dsl(props, dsl) + sc = P('Shortcut') + if sc: + dsl['shortcut'] = sc + cpt = get_type_shorthand(_single(props, 'md:CommandParameterType')) + if cpt: + dsl['commandParameterType'] = cpt + add_enum_prop('parameterUseMode', 'ParameterUseMode', 'Single') + add_bool_prop('modifiesData', 'ModifiesData', False) + add_enum_prop('onMainServerUnavalableBehavior', 'OnMainServerUnavalableBehavior', 'Auto') + # CommonAttribute. + if obj_type == 'CommonAttribute': + vt = get_type_shorthand(_single(props, 'md:Type')) + if vt and vt != 'String(0)': + dsl['valueType'] = vt + add_bool_prop('passwordMode', 'PasswordMode', False) + for mlp in (('Format', 'format'), ('EditFormat', 'editFormat'), ('ToolTip', 'tooltip')): + mv = get_ml_value(_single(props, "md:" + mlp[0])) + if mv is not None: + dsl[mlp[1]] = mv + add_bool_prop('markNegatives', 'MarkNegatives', False) + msk = P('Mask') + if msk: + dsl['mask'] = msk + add_bool_prop('multiLine', 'MultiLine', False) + add_bool_prop('extendedEdit', 'ExtendedEdit', False) + for mm in (('MinValue', 'minValue'), ('MaxValue', 'maxValue')): + mn = _single(props, "md:" + mm[0]) + if mn is not None and _attr(mn, 'nil', NS_XSI) != 'true': + mxt = _attr(mn, 'type', NS_XSI) + if re.search(r'decimal$', mxt, re.I): + dsl[mm[1]] = int(_text(mn)) if re.match(r'^-?\d+$', _text(mn)) else float(_text(mn)) + else: + dsl[mm[1]] = _text(mn) + add_bool_prop('fillFromFillingValue', 'FillFromFillingValue', False) + # FillValue: тип-зависимый дефолт (String→typed-empty, Number→0, прочее→nil). + cat_vt = vt if vt else 'String(0)' + fv_n = _single(props, 'md:FillValue') + if fv_n is not None: + fv_nil = (_attr(fv_n, 'nil', NS_XSI) == 'true') + if fv_nil: + if re.match(r'^(String|Number)', cat_vt): + dsl['fillValue'] = {'nil': True} + else: + fv_xt = _attr(fv_n, 'type', NS_XSI) + if re.search(r'DesignTimeRef$', fv_xt, re.I) and _text(fv_n) == '': + dsl['fillValue'] = {'emptyRef': True} + elif re.search(r'decimal$', fv_xt, re.I): + if _text(fv_n) != '0': + dsl['fillValue'] = int(_text(fv_n)) if re.match(r'^-?\d+$', _text(fv_n)) else float(_text(fv_n)) + elif _text(fv_n): + dsl['fillValue'] = _text(fv_n) + add_enum_prop('fillChecking', 'FillChecking', 'DontCheck') + add_enum_prop('choiceFoldersAndItems', 'ChoiceFoldersAndItems', 'Items') + cpl = parse_choice_parameter_links(props, 'md:ChoiceParameterLinks') + if cpl is not None: + dsl['choiceParameterLinks'] = cpl + cp = parse_choice_parameters(props, 'md:ChoiceParameters') + if cp is not None: + dsl['choiceParameters'] = cp + add_enum_prop('quickChoice', 'QuickChoice', 'Auto') + add_enum_prop('createOnInput', 'CreateOnInput', 'Auto') + cf = P('ChoiceForm') + if cf: + dsl['choiceForm'] = cf + add_enum_prop('choiceHistoryOnInput', 'ChoiceHistoryOnInput', 'Auto') + cn = _single(props, 'md:Content') + if cn is not None: + c_arr = [] + for it in _nodes(cn, 'xr:Item'): + md_n = _single(it, 'xr:Metadata') + mdv = _text(md_n) if md_n is not None else '' + use_n = _single(it, 'xr:Use') + usev = _text(use_n) if use_n is not None else 'Use' + cs_n = _single(it, 'xr:ConditionalSeparation') + csv = _text(cs_n) if cs_n is not None else '' + if usev == 'Use' and not csv: + c_arr.append(mdv) + else: + io = {'metadata': mdv} + if usev != 'Use': + io['use'] = usev + if csv: + io['conditionalSeparation'] = csv + c_arr.append(io) + if len(c_arr) > 0: + dsl['content'] = c_arr + add_enum_prop('autoUse', 'AutoUse', 'DontUse') + add_enum_prop('dataSeparation', 'DataSeparation', 'DontUse') + add_enum_prop('separatedDataUse', 'SeparatedDataUse', 'Independently') + dsv = P('DataSeparationValue') + if dsv: + dsl['dataSeparationValue'] = dsv + dsu = P('DataSeparationUse') + if dsu: + dsl['dataSeparationUse'] = dsu + cs2 = P('ConditionalSeparation') + if cs2: + dsl['conditionalSeparation'] = cs2 + add_enum_prop('usersSeparation', 'UsersSeparation', 'DontUse') + add_enum_prop('authenticationSeparation', 'AuthenticationSeparation', 'DontUse') + add_enum_prop('configurationExtensionsSeparation', 'ConfigurationExtensionsSeparation', 'DontUse') + add_enum_prop('indexing', 'Indexing', 'DontIndex') + add_enum_prop('dataHistory', 'DataHistory', 'Use') + # ScheduledJob. + if obj_type == 'ScheduledJob': + mn = P('MethodName') + if mn: + dsl['methodName'] = mn + descr = P('Description') + if descr: + dsl['description'] = descr + k = P('Key') + if k: + dsl['key'] = k + add_bool_prop('use', 'Use', False) + add_bool_prop('predefined', 'Predefined', False) + add_int_prop('restartCountOnFailure', 'RestartCountOnFailure', 3) + add_int_prop('restartIntervalOnFailure', 'RestartIntervalOnFailure', 10) + # Constant — богатый одиночный реквизит. + if obj_type == 'Constant': + vt = get_type_shorthand(_single(props, 'md:Type')) + if vt: + dsl['valueType'] = vt + else: + dsl['valueType'] = '' + dfm = P('DefaultForm') + if dfm: + dsl['defaultForm'] = dfm + ep = get_ml_value(_single(props, 'md:ExtendedPresentation')) + if ep is not None: + dsl['extendedPresentation'] = ep + add_bool_prop('passwordMode', 'PasswordMode', False) + fmt = get_ml_value(_single(props, 'md:Format')) + if fmt is not None: + dsl['format'] = fmt + efmt = get_ml_value(_single(props, 'md:EditFormat')) + if efmt is not None: + dsl['editFormat'] = efmt + tt = get_ml_value(_single(props, 'md:ToolTip')) + if tt is not None: + dsl['tooltip'] = tt + add_bool_prop('markNegatives', 'MarkNegatives', False) + msk = P('Mask') + if msk: + dsl['mask'] = msk + add_bool_prop('multiLine', 'MultiLine', False) + add_bool_prop('extendedEdit', 'ExtendedEdit', False) + for mm in (('MinValue', 'minValue'), ('MaxValue', 'maxValue')): + mn = _single(props, "md:" + mm[0]) + if mn is not None and _attr(mn, 'nil', NS_XSI) != 'true': + mxt = _attr(mn, 'type', NS_XSI) + if re.search(r'decimal$', mxt, re.I): + dsl[mm[1]] = int(_text(mn)) if re.match(r'^-?\d+$', _text(mn)) else float(_text(mn)) + else: + dsl[mm[1]] = _text(mn) + add_enum_prop('fillChecking', 'FillChecking', 'DontCheck') + add_enum_prop('choiceFoldersAndItems', 'ChoiceFoldersAndItems', 'Items') + cpl = parse_choice_parameter_links(props, 'md:ChoiceParameterLinks') + if cpl is not None: + dsl['choiceParameterLinks'] = cpl + cp = parse_choice_parameters(props, 'md:ChoiceParameters') + if cp is not None: + dsl['choiceParameters'] = cp + add_enum_prop('quickChoice', 'QuickChoice', 'Auto') + cf = P('ChoiceForm') + if cf: + dsl['choiceForm'] = cf + lbt_node = _single(props, 'md:LinkByType') + if lbt_node is not None: + dp_n = _single(lbt_node, 'md:DataPath') + if dp_n is not None and _text(dp_n): + li_n = _single(lbt_node, 'md:LinkItem') + li = int(_text(li_n)) if (li_n is not None and _text(li_n)) else 0 + dsl['linkByType'] = _text(dp_n) if li == 0 else {'dataPath': _text(dp_n), 'linkItem': li} + add_enum_prop('dataHistory', 'DataHistory', 'DontUse') + add_bool_prop('updateDataHistoryImmediatelyAfterWrite', 'UpdateDataHistoryImmediatelyAfterWrite', False) + add_bool_prop('executeAfterWriteDataHistoryVersionProcessing', 'ExecuteAfterWriteDataHistoryVersionProcessing', False) + + # InputByString — эмитим только при отличии от выведенного дефолта. + ib_node = _single(props, 'md:InputByString') + if ib_node is not None: + ib_actual = [_text(x) for x in _nodes(ib_node, 'xr:Field')] + clv = P('CodeLength') + dlv = P('DescriptionLength') + cl = int(clv) if (clv is not None and clv != '') else 9 + dl = int(dlv) if (dlv is not None and dlv != '') else 25 + ib_def = [] + if dl > 0: + ib_def.append("StandardAttribute.Description") + if cl > 0: + ib_def.append("StandardAttribute.Code") + ib_short = [short_field(x) for x in ib_actual] + same = (len(ib_short) == len(ib_def)) + if same: + for k in range(len(ib_short)): + if ib_short[k] != ib_def[k]: + same = False + break + if not same: + dsl['inputByString'] = list(ib_short) + + # BasedOn. + bo_node = _single(props, 'md:BasedOn') + if bo_node is not None: + bo_items = [_text(x) for x in _nodes(bo_node, 'xr:Item')] + if len(bo_items) > 0: + dsl['basedOn'] = list(bo_items) + + # DataLockFields. + dlf_node = _single(props, 'md:DataLockFields') + if dlf_node is not None: + dlf_fields = [short_field(_text(x)) for x in _nodes(dlf_node, 'xr:Field')] + if len(dlf_fields) > 0: + dsl['dataLockFields'] = list(dlf_fields) + + # Формы по умолчанию (omit-on-empty). + add_form_ref('defaultObjectForm', 'DefaultObjectForm') + add_form_ref('defaultFolderForm', 'DefaultFolderForm') + add_form_ref('defaultListForm', 'DefaultListForm') + add_form_ref('defaultChoiceForm', 'DefaultChoiceForm') + add_form_ref('defaultFolderChoiceForm', 'DefaultFolderChoiceForm') + add_form_ref('auxiliaryObjectForm', 'AuxiliaryObjectForm') + add_form_ref('auxiliaryFolderForm', 'AuxiliaryFolderForm') + add_form_ref('auxiliaryListForm', 'AuxiliaryListForm') + add_form_ref('auxiliaryChoiceForm', 'AuxiliaryChoiceForm') + add_form_ref('auxiliaryFolderChoiceForm', 'AuxiliaryFolderChoiceForm') + add_form_ref('defaultRecordForm', 'DefaultRecordForm') + add_form_ref('auxiliaryRecordForm', 'AuxiliaryRecordForm') + + # Презентации (ML, omit-on-empty). + for pp in (('ObjectPresentation', 'objectPresentation'), ('ExtendedObjectPresentation', 'extendedObjectPresentation'), + ('RecordPresentation', 'recordPresentation'), ('ExtendedRecordPresentation', 'extendedRecordPresentation'), + ('ListPresentation', 'listPresentation'), ('ExtendedListPresentation', 'extendedListPresentation'), + ('Explanation', 'explanation')): + pv = get_ml_value(_single(props, "md:" + pp[0])) + if pv is not None: + dsl[pp[1]] = pv + + # --- Characteristics (привязка ПВХ). --- + chars_node = _single(props, 'md:Characteristics') + if chars_node is not None: + ch_list = _nodes(chars_node, 'xr:Characteristic') + if len(ch_list) > 0: + ch_arr = [] + for ch in ch_list: + ct = _single(ch, 'xr:CharacteristicTypes') + cv = _single(ch, 'xr:CharacteristicValues') + t_from = _attr(ct, 'from') + v_from = _attr(cv, 'from') + + def gt(n, node): + x = _single(node, "xr:" + n) + return _text(x) if x is not None else "" + + def giv(n, node): + x = _single(node, "xr:" + n) + return int(_text(x)) if (x is not None and _text(x) != '') else -1 + + tfv_node = _single(ct, 'xr:TypesFilterValue') + tfv_nil = _attr(tfv_node, 'nil', NS_XSI) if tfv_node is not None else '' + types = { + 'from': t_from, + 'key': shorten_char_field(gt('KeyField', ct), t_from), + 'filterField': shorten_char_field(gt('TypesFilterField', ct), t_from), + 'filterValue': None if tfv_nil == 'true' else convert_ch_scalar_node(tfv_node), + } + dpf = giv('DataPathField', ct) + if dpf != -1: + types['dataPathField'] = dpf + mvu = giv('MultipleValuesUseField', ct) + if mvu != -1: + types['multipleValuesUseField'] = mvu + values = { + 'from': v_from, + 'object': shorten_char_field(gt('ObjectField', cv), v_from), + 'type': shorten_char_field(gt('TypeField', cv), v_from), + 'value': shorten_char_field(gt('ValueField', cv), v_from), + } + mvk = giv('MultipleValuesKeyField', cv) + if mvk != -1: + values['multipleValuesKeyField'] = mvk + mvo = giv('MultipleValuesOrderField', cv) + if mvo != -1: + values['multipleValuesOrderField'] = mvo + ch_arr.append({'types': types, 'values': values}) + dsl['characteristics'] = ch_arr + + # --- StandardAttributes: захватываем ОТКЛОНЕНИЯ от профиля материализованного блока. --- + std_profile_by_type = { + 'Catalog': { + 'Owner': {'fillChecking': 'ShowError', 'fillFromFillingValue': True}, + 'Parent': {'fillFromFillingValue': True}, + 'Description': {'fillChecking': 'ShowError'}, + }, + 'ExchangePlan': { + 'Description': {'fillChecking': 'ShowError'}, + 'Code': {'fillChecking': 'ShowError'}, + }, + 'ChartOfCharacteristicTypes': { + 'Description': {'fillChecking': 'ShowError'}, + 'Parent': {'fillFromFillingValue': True}, + }, + 'ChartOfAccounts': { + 'Description': {'fillChecking': 'ShowError'}, + 'Code': {'fillChecking': 'ShowError'}, + 'Parent': {'fillFromFillingValue': True}, + }, + 'ChartOfCalculationTypes': { + 'Description': {'fillChecking': 'ShowError'}, + }, + 'Document': { + 'Date': {'fillChecking': 'ShowError'}, + }, + } + cat_std_profile = std_profile_by_type.get(obj_type, {}) + std_fixed_by_type = { + 'Catalog': ['PredefinedDataName', 'Predefined', 'Ref', 'DeletionMark', 'IsFolder', 'Owner', 'Parent', 'Description', 'Code'], + 'ExchangePlan': ['Ref', 'DeletionMark', 'Code', 'Description', 'ThisNode', 'SentNo', 'ReceivedNo'], + 'ChartOfCharacteristicTypes': ['PredefinedDataName', 'Predefined', 'Ref', 'DeletionMark', 'Description', 'Code', 'Parent', 'ValueType'], + 'ChartOfAccounts': ['PredefinedDataName', 'Order', 'OffBalance', 'Type', 'Description', 'Code', 'Parent', 'Predefined', 'DeletionMark', 'Ref'], + 'ChartOfCalculationTypes': ['PredefinedDataName', 'Predefined', 'Ref', 'DeletionMark', 'ActionPeriodIsBasic', 'Description', 'Code'], + 'Document': ['Ref', 'DeletionMark', 'Date', 'Number', 'Posted'], + 'Enum': ['Order', 'Ref'], + 'DocumentJournal': ['Type', 'Ref', 'Date', 'Posted', 'DeletionMark', 'Number'], + } + std_fixed = std_fixed_by_type.get(obj_type, []) + std_conditional_types = ['Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document'] + sa_node = _single(props, 'md:StandardAttributes') + if sa_node is not None: + sa_map = {} + for sa in _nodes(sa_node, 'xr:StandardAttribute'): + an = _attr(sa, 'name') + prof = cat_std_profile.get(an, {}) + ov = {} + # FillChecking (профиль или DontCheck) + fc_n = _single(sa, 'xr:FillChecking') + fc = _text(fc_n) if fc_n is not None else 'DontCheck' + prof_fc = prof.get('fillChecking', 'DontCheck') + if fc != prof_fc: + ov['fillChecking'] = fc + # FillFromFillingValue (профиль или false) + ffv_n = _single(sa, 'xr:FillFromFillingValue') + ffv = (ffv_n is not None and _text(ffv_n) == 'true') + prof_ffv = (prof.get('fillFromFillingValue') is True) + if ffv != prof_ffv: + ov['fillFromFillingValue'] = ffv + # Synonym / ToolTip (профиль пуст) + syn = get_ml_value(_single(sa, 'xr:Synonym')) + if syn is not None: + ov['synonym'] = syn + tt = get_ml_value(_single(sa, 'xr:ToolTip')) + if tt is not None: + ov['tooltip'] = tt + # FullTextSearch / DataHistory (профиль = Use) + fts_n = _single(sa, 'xr:FullTextSearch') + if fts_n is not None and _text(fts_n) != 'Use': + ov['fullTextSearch'] = _text(fts_n) + dh_n = _single(sa, 'xr:DataHistory') + if dh_n is not None and _text(dh_n) != 'Use': + ov['dataHistory'] = _text(dh_n) + # FillValue (дефолт nil). Comment/Mask/ChoiceForm (дефолт пусто). + fv_n = _single(sa, 'xr:FillValue') + if fv_n is not None and _attr(fv_n, 'nil', NS_XSI) != 'true': + fv_xt = _attr(fv_n, 'type', NS_XSI) + if re.search(r'DesignTimeRef$', fv_xt, re.I) and _text(fv_n) == '': + ov['fillValue'] = {'emptyRef': True} + else: + ov['fillValue'] = convert_ch_scalar_node(fv_n) + sa_cmt = _single(sa, 'xr:Comment') + if sa_cmt is not None and _text(sa_cmt): + ov['comment'] = _text(sa_cmt) + sa_msk = _single(sa, 'xr:Mask') + if sa_msk is not None and _text(sa_msk): + ov['mask'] = _text(sa_msk) + sa_fmt = get_ml_value(_single(sa, 'xr:Format')) + if sa_fmt is not None: + ov['format'] = sa_fmt + sa_efmt = get_ml_value(_single(sa, 'xr:EditFormat')) + if sa_efmt is not None: + ov['editFormat'] = sa_efmt + sa_cf = _single(sa, 'xr:ChoiceForm') + if sa_cf is not None and _text(sa_cf): + ov['choiceForm'] = _text(sa_cf) + sa_cpl = parse_choice_parameter_links(sa, 'xr:ChoiceParameterLinks') + if sa_cpl is not None: + ov['choiceParameterLinks'] = sa_cpl + sa_cp = parse_choice_parameters(sa, 'xr:ChoiceParameters') + if sa_cp is not None: + ov['choiceParameters'] = sa_cp + # LinkByType стандартного реквизита (ExtDimensionN→Account у регистра бухгалтерии). + sa_lbt = _single(sa, 'xr:LinkByType') + if sa_lbt is not None: + sa_lbt_dp = _single(sa_lbt, 'xr:DataPath') + if sa_lbt_dp is not None and _text(sa_lbt_dp): + sa_lbt_li = _single(sa_lbt, 'xr:LinkItem') + li = int(_text(sa_lbt_li)) if (sa_lbt_li is not None and _text(sa_lbt_li)) else 0 + ov['linkByType'] = {'dataPath': _text(sa_lbt_dp), 'linkItem': li} + # Доп./опциональный реквизит (не в фикс-списке) — эмитим по присутствию даже без отклонений. + if len(ov) > 0 or (an not in std_fixed): + sa_map[an] = ov + if len(sa_map) > 0 or (obj_type in std_conditional_types): + dsl['standardAttributes'] = sa_map + elif obj_type in ('InformationRegister', 'AccumulationRegister', 'AccountingRegister', 'CalculationRegister', 'BusinessProcess', 'Task', 'Enum', 'DocumentJournal'): + # Регистр/БП/Задача опускают all-default блок → opt-out `standardAttributes:""`. + dsl['standardAttributes'] = '' + + # --- ChildObjects: Attributes + TabularSections --- + child_objs = _single(obj_node, 'md:ChildObjects') + if child_objs is not None: + attrs = _nodes(child_objs, 'md:Attribute') + if len(attrs) > 0: + arr = [] + for a in attrs: + arr.append(attr_to_dsl(a)) + dsl['attributes'] = arr + # Enum: значения перечисления. + ev_nodes = _nodes(child_objs, 'md:EnumValue') + if len(ev_nodes) > 0: + ev_arr = [] + for ev in ev_nodes: + evp = _single(ev, 'md:Properties') + ev_name = _text(_single(evp, 'md:Name')) + ev_syn_node = _single(evp, 'md:Synonym') + ev_syn = get_ml_value(ev_syn_node) + ev_cmt_n = _single(evp, 'md:Comment') + ev_cmt = _text(ev_cmt_n) if ev_cmt_n is not None else '' + ev_syn_val = None + if isinstance(ev_syn, str): + if ne_ci(ev_syn, split_camel_words(ev_name)): + ev_syn_val = ev_syn + elif ev_syn is not None: + ev_syn_val = ev_syn + elif ev_syn_node is not None: + ev_syn_val = '' + if (ev_syn_val is not None) or ev_cmt: + o = {'name': ev_name} + if ev_syn_val is not None: + o['synonym'] = ev_syn_val + if ev_cmt: + o['comment'] = ev_cmt + ev_arr.append(o) + else: + ev_arr.append(ev_name) + dsl['values'] = ev_arr + # DocumentJournal: колонки. + col_nodes = _nodes(child_objs, 'md:Column') + if len(col_nodes) > 0: + col_arr = [] + for col in col_nodes: + cp = _single(col, 'md:Properties') + c_name = _text(_single(cp, 'md:Name')) + o = {'name': c_name} + c_syn_node = _single(cp, 'md:Synonym') + c_syn = get_ml_value(c_syn_node) + if isinstance(c_syn, str): + if ne_ci(c_syn, split_camel_words(c_name)): + o['synonym'] = c_syn + elif c_syn is not None: + o['synonym'] = c_syn + elif c_syn_node is not None: + o['synonym'] = '' # пустой ≠ авто-синоним → явный '' + c_cmt_n = _single(cp, 'md:Comment') + if c_cmt_n is not None and _text(c_cmt_n): + o['comment'] = _text(c_cmt_n) + c_idx_n = _single(cp, 'md:Indexing') + if c_idx_n is not None and _text(c_idx_n) != 'DontIndex': + o['indexing'] = _text(c_idx_n) + c_ref_node = _single(cp, 'md:References') + refs = [] + if c_ref_node is not None: + for it in _nodes(c_ref_node, 'xr:Item'): + refs.append(_text(it)) + o['references'] = refs + col_arr.append(o) + dsl['columns'] = col_arr + # ChartOfAccounts: признаки учёта. + acct_flag_nodes = _nodes(child_objs, 'md:AccountingFlag') + if len(acct_flag_nodes) > 0: + arr = [] + for a in acct_flag_nodes: + arr.append(attr_to_dsl(a)) + dsl['accountingFlags'] = arr + ext_dim_flag_nodes = _nodes(child_objs, 'md:ExtDimensionAccountingFlag') + if len(ext_dim_flag_nodes) > 0: + arr = [] + for a in ext_dim_flag_nodes: + arr.append(attr_to_dsl(a)) + dsl['extDimensionAccountingFlags'] = arr + # Sequence: измерения несут DocumentMap/RegisterRecordsMap. + dim_nodes = _nodes(child_objs, 'md:Dimension') + if len(dim_nodes) > 0 and obj_type == 'Sequence': + arr = [] + for dn in dim_nodes: + dp = _single(dn, 'md:Properties') + d_name = _text(_single(dp, 'md:Name')) + o = {'name': d_name} + d_syn = get_ml_value(_single(dp, 'md:Synonym')) + if isinstance(d_syn, str): + if ne_ci(d_syn, split_camel_words(d_name)): + o['synonym'] = d_syn + elif d_syn is not None: + o['synonym'] = d_syn + d_cmt_n = _single(dp, 'md:Comment') + if d_cmt_n is not None and _text(d_cmt_n): + o['comment'] = _text(d_cmt_n) + dt = get_type_shorthand(_single(dp, 'md:Type')) + if dt: + o['type'] = dt + for mp in (('DocumentMap', 'documentMap'), ('RegisterRecordsMap', 'registerRecordsMap')): + mn = _single(dp, "md:" + mp[0]) + if mn is not None: + m_items = [_text(x) for x in _nodes(mn, 'xr:Item')] + if len(m_items) > 0: + o[mp[1]] = list(m_items) + arr.append(o) + dsl['dimensions'] = arr + elif len(dim_nodes) > 0: + arr = [] + for a in dim_nodes: + arr.append(attr_to_dsl(a)) + dsl['dimensions'] = arr + res_nodes = _nodes(child_objs, 'md:Resource') + if len(res_nodes) > 0: + arr = [] + for a in res_nodes: + arr.append(attr_to_dsl(a)) + dsl['resources'] = arr + # Задача: реквизиты адресации. + addr_nodes = _nodes(child_objs, 'md:AddressingAttribute') + if len(addr_nodes) > 0: + arr = [] + for a in addr_nodes: + arr.append(attr_to_dsl(a)) + dsl['addressingAttributes'] = arr + ts_nodes = _nodes(child_objs, 'md:TabularSection') + if len(ts_nodes) > 0: + ts_map = {} + for ts in ts_nodes: + tsp = _single(ts, 'md:Properties') + ts_name = _text(_single(tsp, 'md:Name')) + tco = _single(ts, 'md:ChildObjects') + cols = [] + if tco is not None: + for ca in _nodes(tco, 'md:Attribute'): + cols.append(attr_to_dsl(ca)) + # Синоним/подсказка/комментарий ТЧ. + ts_syn = get_ml_value(_single(tsp, 'md:Synonym')) + ts_syn_custom = False + if isinstance(ts_syn, str): + if ne_ci(ts_syn, split_camel_words(ts_name)): + ts_syn_custom = True + elif ts_syn is not None: + ts_syn_custom = True + ts_tt = get_ml_value(_single(tsp, 'md:ToolTip')) + ts_cmt_n = _single(tsp, 'md:Comment') + ts_cmt = _text(ts_cmt_n) if ts_cmt_n is not None else '' + ts_fc_n = _single(tsp, 'md:FillChecking') + ts_fc = _text(ts_fc_n) if (ts_fc_n is not None and _text(ts_fc_n) != 'DontCheck') else '' + # TS-блок стандартных реквизитов (LineNumber). + ln_obj = {} + sa_ts_node = _single(tsp, 'md:StandardAttributes') + has_block = (sa_ts_node is not None and len(_nodes(sa_ts_node, 'xr:StandardAttribute')) > 0) + ln_node = _single(sa_ts_node, "xr:StandardAttribute[@name='LineNumber']") if has_block else None + if ln_node is not None: + ln_syn = get_ml_value(_single(ln_node, 'xr:Synonym')) + if ln_syn is not None: + ln_obj['synonym'] = ln_syn + ln_cmt_n = _single(ln_node, 'xr:Comment') + if ln_cmt_n is not None and _text(ln_cmt_n): + ln_obj['comment'] = _text(ln_cmt_n) + ln_fts_n = _single(ln_node, 'xr:FullTextSearch') + if ln_fts_n is not None and _text(ln_fts_n) != 'Use': + ln_obj['fullTextSearch'] = _text(ln_fts_n) + ln_tt = get_ml_value(_single(ln_node, 'xr:ToolTip')) + if ln_tt is not None: + ln_obj['tooltip'] = ln_tt + ln_fmt = get_ml_value(_single(ln_node, 'xr:Format')) + if ln_fmt is not None: + ln_obj['format'] = ln_fmt + ln_efmt = get_ml_value(_single(ln_node, 'xr:EditFormat')) + if ln_efmt is not None: + ln_obj['editFormat'] = ln_efmt + ln_chi_n = _single(ln_node, 'xr:ChoiceHistoryOnInput') + if ln_chi_n is not None and _text(ln_chi_n) != 'Auto': + ln_obj['choiceHistoryOnInput'] = _text(ln_chi_n) + ln_fv_n = _single(ln_node, 'xr:FillValue') + if ln_fv_n is not None and _attr(ln_fv_n, 'nil', NS_XSI) != 'true': + ln_fv_t = _attr(ln_fv_n, 'type', NS_XSI) + if re.search(r'decimal$', ln_fv_t, re.I): + ln_obj['fillValue'] = int(_text(ln_fv_n)) if re.match(r'^-?\d+$', _text(ln_fv_n)) else float(_text(ln_fv_n)) + if ts_syn_custom or (ts_tt is not None) or ts_cmt or ts_fc or len(ln_obj) > 0 or (not has_block): + to = {} + if ts_syn_custom: + to['synonym'] = ts_syn + if ts_tt is not None: + to['tooltip'] = ts_tt + if ts_cmt: + to['comment'] = ts_cmt + if ts_fc: + to['fillChecking'] = ts_fc + if not has_block: + to['lineNumber'] = '' + elif len(ln_obj) > 0: + to['lineNumber'] = ln_obj + to['attributes'] = cols + ts_map[ts_name] = to + else: + ts_map[ts_name] = cols + dsl['tabularSections'] = ts_map + # --- Commands (полноблочные в ChildObjects) → DSL commands. --- + cmd_nodes = _nodes(child_objs, 'md:Command') + if len(cmd_nodes) > 0: + cmd_map = {} + for cm in cmd_nodes: + cp = _single(cm, 'md:Properties') + cn = _text(_single(cp, 'md:Name')) + o = {} + syn = get_ml_value(_single(cp, 'md:Synonym')) + if isinstance(syn, str): + if ne_ci(syn, split_camel_words(cn)): + o['synonym'] = syn + elif syn is not None: + o['synonym'] = syn + cmt_n = _single(cp, 'md:Comment') + if cmt_n is not None and _text(cmt_n): + o['comment'] = _text(cmt_n) + grp_n = _single(cp, 'md:Group') + if grp_n is not None and _text(grp_n): + o['group'] = _text(grp_n) + cpt = get_type_shorthand(_single(cp, 'md:CommandParameterType')) + if cpt: + o['commandParameterType'] = cpt + pum_n = _single(cp, 'md:ParameterUseMode') + if pum_n is not None and _text(pum_n) != 'Single': + o['parameterUseMode'] = _text(pum_n) + md_n = _single(cp, 'md:ModifiesData') + if md_n is not None and _text(md_n) == 'true': + o['modifiesData'] = True + rep_n = _single(cp, 'md:Representation') + if rep_n is not None and _text(rep_n) != 'Auto': + o['representation'] = _text(rep_n) + ctt = get_ml_value(_single(cp, 'md:ToolTip')) + if ctt is not None: + o['tooltip'] = ctt + # — структурный блок. + ref_n = _single(cp, 'md:Picture/xr:Ref') + abs_n = _single(cp, 'md:Picture/xr:Abs') + if ref_n is not None or abs_n is not None: + psrc = _text(ref_n) if ref_n is not None else ("abs:" + _text(abs_n)) + lt_n = _single(cp, 'md:Picture/xr:LoadTransparent') + lt_false = (lt_n is not None and _text(lt_n) == 'false') + tpx_n = _single(cp, 'md:Picture/xr:TransparentPixel') + if tpx_n is not None: + po = {'src': psrc} + if lt_false: + po['loadTransparent'] = False + po['transparentPixel'] = {'x': int(_attr(tpx_n, 'x')), 'y': int(_attr(tpx_n, 'y'))} + o['picture'] = po + else: + o['picture'] = psrc + if lt_false: + o['loadTransparent'] = False + sc_n = _single(cp, 'md:Shortcut') + if sc_n is not None and _text(sc_n): + o['shortcut'] = _text(sc_n) + osu_n = _single(cp, 'md:OnMainServerUnavalableBehavior') + if osu_n is not None and _text(osu_n) != 'Auto': + o['onMainServerUnavalableBehavior'] = _text(osu_n) + cmd_map[cn] = o + dsl['commands'] = cmd_map + + # --- Предопределённые (соседний Ext/Predefined.xml) → DSL predefined. --- + obj_dir = os.path.dirname(os.path.abspath(object_path)) + predef_path = os.path.join(obj_dir, obj_name, 'Ext', 'Predefined.xml') + if os.path.isfile(predef_path): + pdoc = etree.parse(predef_path).getroot() + root_items = [] + if obj_type == 'ChartOfAccounts': + for it in _lxn(pdoc, "*[local-name()='Item']"): + root_items.append(predef_account_to_dsl(it)) + elif obj_type == 'ChartOfCalculationTypes': + for it in _lxn(pdoc, "*[local-name()='Item']"): + root_items.append(predef_calc_type_to_dsl(it)) + else: + for it in _lxn(pdoc, "*[local-name()='Item']"): + root_items.append(predef_item_to_dsl(it)) + if len(root_items) > 0: + dsl['predefined'] = root_items + + # --- Состав плана обмена (соседний Ext/Content.xml) → DSL content (ExchangePlan). --- + if obj_type == 'ExchangePlan': + content_path = os.path.join(obj_dir, obj_name, 'Ext', 'Content.xml') + if os.path.isfile(content_path): + cdoc = etree.parse(content_path).getroot() + content_items = [] + for it in _lxn(cdoc, "*[local-name()='Item']"): + md_el = _lx1(it, "*[local-name()='Metadata']") + if md_el is None or not _text(md_el): + continue + ref = _text(md_el) + ar_el = _lx1(it, "*[local-name()='AutoRecord']") + ar = _text(ar_el) if ar_el is not None else 'Deny' + if ar == 'Allow': + content_items.append("%s: autoRecord" % ref) + else: + content_items.append(ref) + if len(content_items) > 0: + dsl['content'] = content_items + + +SUPPORTED_TYPES = ( + 'Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document', + 'InformationRegister', 'AccumulationRegister', 'AccountingRegister', 'CalculationRegister', 'BusinessProcess', 'Task', + 'Enum', 'Report', 'DataProcessor', 'Constant', 'DefinedType', 'FunctionalOption', 'DocumentJournal', 'Sequence', + 'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob', + 'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', + 'WSReference', 'CommonPicture', 'CommonTemplate', +) + + +def main(): + global props, obj_node, obj_type, obj_name, object_path + + parser = argparse.ArgumentParser(description='Decompile 1C metadata object XML to JSON DSL (draft)', allow_abbrev=False) + parser.add_argument('-ObjectPath', '-Path', dest='ObjectPath', type=str, required=True) + parser.add_argument('-OutputPath', dest='OutputPath', type=str, default=None) + args = parser.parse_args() + + object_path = args.ObjectPath + if not os.path.exists(object_path): + sys.stderr.write("meta-decompile: файл не найден: %s\n" % object_path) + sys.exit(2) + + # --- XML загрузка --- + doc = etree.parse(object_path) + root_el = doc.getroot() + if _localname(root_el) != 'MetaDataObject': + sys.stderr.write("meta-decompile: ожидался root , получен <%s>\n" % _localname(root_el)) + sys.exit(3) + # Первый элемент-потомок MetaDataObject = объект; его LocalName = тип. + obj_node = None + for c in root_el: + if isinstance(c.tag, str): + obj_node = c + break + if obj_node is None: + sys.stderr.write("meta-decompile: пустой MetaDataObject\n") + sys.exit(3) + obj_type = _localname(obj_node) + + if obj_type not in SUPPORTED_TYPES: + sys.stderr.write("meta-decompile: тип '%s' пока не поддержан (…, CommonPicture, CommonTemplate)\n" % obj_type) + sys.exit(3) + + props = _single(obj_node, 'md:Properties') + obj_name = P('Name') + + build_dsl() + + # === Вывод === + json_str = convert_to_compact_json(dsl, 0) + if args.OutputPath: + with open(args.OutputPath, 'w', encoding='utf-8', newline='') as f: + f.write(json_str) + else: + sys.stdout.write(json_str + "\n") + + +if __name__ == '__main__': + main()