Compare commits

...

8 Commits

Author SHA1 Message Date
Nick Shirokov 3eaa7ffa3b fix(skd-edit): drop unneeded " → " in query/expression
Зеркалим решение из skd-compile: убираем .Replace('"','"') из Esc-Xml
и удаляем post-process, который принудительно ставил " внутри
<query>/<expression>. Реальный Конфигуратор так не пишет — экранирование
было анти-1С-стилем и портило round-trip diff.

Снимок add-calculated-field-restrict обновлён под новый формат.
Кейс preserve-entities-modify-parameter-title удалён: его смысл
инвертировался (теперь проверял бы нормализацию, а не сохранение),
а часть про многострочный xmlns уже покрыта preserve-xmlns-multiline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:51:04 +03:00
Nick Shirokov 98ebb478ee fix(skd-compile): drop unneeded " → &quot; — matches Designer style
Конфигуратор внутри текстового контента <query>/<expression> оставляет " сырыми
(проверено на ERP DCS: 1504 raw " против 0 &quot;). Убираем .Replace('"','&quot;')
из esc_xml — теперь round-trip diff против типовых остаётся чистым.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:50:55 +03:00
Nick Shirokov fb67b1b80d fix(skd-edit): realistic multilang fixture (ERP-style appearance block)
multilang-base/Template.xml содержал <editFormat xsi:type="v8:LocalStringType">
на <field xsi:type="DataSetFieldField">, что нелегально по XDTO-схеме DCS —
1С Designer падал с "Исключение XDTO" при загрузке через
LoadExternalDataProcessorOrReportFromFiles. Snapshot-тесты этого не ловили
(только byte-equality), а platform-verify (tests/skills/verify-snapshots.mjs)
ронялся на трёх кейсах с этой фикстурой.

Заменил <editFormat> на реалистичный <appearance> блок с вложенным
<dcscor:item xsi:type="dcsset:SettingsParameterValue"> и многоязычным
<dcscor:value> (ru + en) — структура взята из типовой ERP-выгрузки. Это
даёт более правильный test для preserve-unknown-children: <appearance>
содержит вложенный multi-lang xsi:type-узел, который точно прошёл бы
через DOM round-trip с искажениями, если бы _unknownChildren не работал.

preserve-unknown-children-modify-field: shorthand изменён с
"@ignoreNullsInGroups" на "@dimension" (no-op по составу role, но
триггерит rebuild). Прежний @ignoreNullsInGroups без @dimension давал
комбинацию, которую Designer отвергает (ignoreNullsInGroups валиден
только в контексте resource-роли).

39/39 snapshot suite (PS+PY) + 39/39 platform verify через erf-build →
Designer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:19:13 +03:00
Nick Shirokov 79db5de6ee fix(skd-edit): preserve multi-lang title + unknown children in modify-*
В типовых конфигурациях (ERP, БП, ЗУП и т.д.) у полей и параметров обычно
есть мульти-язык title (ru + en, иногда + локализация). До этого modify-field /
modify-parameter / modify-dataParameter, перестраивая элемент через
Build-MLTextXml, оставляли только последнее найденное <v8:content> в ru —
en/uk/kk siblings молча терялись, и при следующей выгрузке Designer
ломал миграцию.

Read-FieldProperties сохраняет полный OuterXml <title> в _rawTitle и
коллекционирует OuterXml неизвестных дочерних элементов
(<editFormat>, <appearance>, кастомные расширения) в _unknownChildren.
Build-FieldFragment эмитит:
* _rawTitle как есть, если user не задал новый title;
* Patch-MLTextRu(_rawTitle, newRu) если user задал ru-override — патчит
  только <v8:content> в <v8:lang>ru</v8:lang>, остальные языки сохраняет;
* _unknownChildren в конце поля (после valueType).

modify-parameter аналогично: при title-override проверяет multi-lang
(>1 <v8:item>) и патчит ru через Patch-MLTextRu, иначе ребилдит ru-only.

set-field-role сохраняет нестандартные подэлементы <role> (например
<dcscom:addition>, <dcscom:groupFields>), не входящие в фиксированный
known-children set и не указанные через kv в shorthand.

xmlns-стрип на захваченных OuterXml — лишние декларации (которые сериализаторы
добавляют для standalone-фрагментов) убираются.

PY: lxml etree.tostring по умолчанию включает .tail (whitespace после
закрывающего тега), что приводило к non-idempotent ростy whitespace при
повторных прогонах. Везде добавлен with_tail=False.

Новые тесты с idempotent: true:
* preserve-multilang-modify-field (ru-override на multi-lang title);
* preserve-multilang-modify-parameter (то же для параметра);
* preserve-unknown-children-modify-field (role flag, проверяем что
  <editFormat> и en title не теряются).

Общая fixture: multilang-base/Template.xml с полем и параметром,
у каждого ru + en title; поле также имеет <editFormat>.

39/39 PS + 39/39 PY. skd-edit v1.20 -> v1.21.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:18:07 +03:00
Nick Shirokov 23d2cb42de fix(skd-edit): preserve <valueType>, detect line endings, drop CRLF leak
Targeted follow-ups к round-trip фиксу:

* modify-field больше не теряет <valueType> при перестройке поля —
  Read-FieldProperties сохраняет полный OuterXml элемента (StringQualifiers,
  NumberQualifiers, DateQualifiers и т.п.), Build-FieldFragment отдаёт его
  обратно. Лишние xmlns-декларации, добавляемые сериализатором при
  выгрузке поддерева, стрипаются регексом.
* Line-ending convention теперь определяется при load (CRLF vs LF) и
  единообразно применяется в финале save. Раньше CreateWhitespace и
  Build-*Fragment везде использовали CRLF, что приводило к смешанным
  переносам в LF-исходниках (и наоборот) и к non-idempotent выходу
  modify-parameter title (run 1 → \n\t\t<title>\r\n... → run 2 →
  \r\n\t\t<title>\r\n...).
* PS Insert-BeforeElement переведён на LF; все -join "`r`n" → "`n";
  py "\r\n".join → "\n". Конечная нормализация переносов делается в
  save в соответствии со script:LineEnding.
* preserve-entities-modify-parameter-title.json теперь idempotent: true
  (после фикса CRLF leak'а двойной прогон byte-identical).

На реальной схеме diff после modify-field составил 30 строк: целевая
вставка title плюс полезная одноразовая коррекция ранее повреждённых
&quot; в text-content <dcsat:expression>. modify-field идемпотентен.

skd-edit v1.19 -> v1.20.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:18:07 +03:00
Nick Shirokov 511bfe7fdf fix(skd-edit): NO-OP skip + format-preserve post-process (round-trip)
XmlDocument round-trip искажал Template.xml даже при отсутствии правок:
декодировал &quot; в <query>/<expression>, схлопывал многострочный xmlns
корня, добавлял пробел перед /> и записывал файл при [WARN] not found.

Дирти-флаг ($script:Dirty / dirty) ставится только на успешной мутации;
финальный save пропускается с [INFO] No changes -- file untouched, если
ни одна операция в batch ничего не изменила. Post-process после OuterXml
восстанавливает raw-форматирование корневого xmlns из исходного файла,
re-escape `"` в текстах <query>/<expression> с anchored regex (не задевая
xsi:type="..."), и нормализует <foo .../> к <foo.../>.

Замеры на реальной схеме после modify-field: diff упал с 423 строк до 37
(94% шума устранено), повторный прогон byte-identical.

В runner.mjs добавлен caseData.idempotent: re-run + byte-equality на всех
файлах workDir. Три новых кейса (NO-OP, entity-preserve, xmlns-multiline)
+ общий fixture roundtrip-base. Все 33 ранее существовавших snapshot
перегенерированы под корректное форматирование (восстанавливают то, что
старый skd-edit ломал).

skd-edit v1.18 -> v1.19. PS и PY порты синхронизированы.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:17:42 +03:00
Nick Shirokov f91b569564 docs(web-test): спецификация регресс-движка + чистка regress.md
Новый канонический документ docs/web-test-regression-spec.md —
техническое описание движка регрессионных тестов: CLI, формат
тест-модулей, ctx-контракт, утверждения, три уровня хуков
(инфра/тест/контекст), конфиг, контексты Playwright и режимы
изоляции, форматы отчётов (JSON/Allure/JUnit), обнаружение тестов,
ошибки/таймауты/повторы, анализ результатов, глоссарий.

Документ предназначен для CI-интеграторов, ручного редактирования
сгенерированных тестов и сопровождения самого движка. Без дорожной
карты и внутренних self-тестов — только публичный контракт.

regress.md в скилле почищен: добавлены контракт ctx и список
утверждений (раньше модели приходилось читать исходники), срезаны
дубликаты с SKILL.md (live recon, паттерны catalog/document),
переформулированы анти-паттерны под специфику регресс-движка.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:01:38 +03:00
Nick Shirokov e93185c18b fix(web-test): сериализовать onecError и платформенный стек 1С в JSON / Allure
В тест-обёртке ACTION_FN при 1С-исключении на throw-ed Error
вешалась полная структура (step, args, errors, formState, stack,
screenshot), но при сборке отчёта движок брал из неё только
{message, step, screenshot} — остальные поля терялись. Платформенный
стек 1С, ради которого делается fetchErrorStack, в JSON-отчёт не
попадал; в Allure statusDetails.trace писался только log()-вывод
теста.

Что поменялось:
- errInfo собирается один раз после teardown (раньше был дубликат на
  732 и 745), используется и для ctx.testResult (afterEach), и для
  lastError, и для итоговой записи в results[].
- В errInfo добавлено поле onecError: e.onecError — структура с
  stack.entries[{location, code}], formState, args, errors доезжает
  до JSON-отчёта без обрезания.
- writeAllure склеивает statusDetails.trace из tr.output + (если есть)
  onecError.stack.raw под разделителем "--- 1C stack ---". В Allure UI
  платформенный стек теперь виден прямо в карточке упавшего теста.

Обратная совместимость: для падений без 1С-исключения (assertion,
навигация и т.п.) e.onecError === undefined → JSON.stringify его
выкидывает, форма записи { message, screenshot } сохраняется в
точности.

Проверено вручную на стенде tests/web-test/ — падающий тест с
ВызватьИсключение, JSON и Allure оба содержат полный stack.entries и
формированный trace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:01:25 +03:00
54 changed files with 2615 additions and 454 deletions
@@ -1,4 +1,4 @@
# skd-compile v1.22 — Compile 1C DCS from JSON
# skd-compile v1.23 — Compile 1C DCS from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$DefinitionFile,
@@ -55,7 +55,7 @@ function X {
function Esc-Xml {
param([string]$s)
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;').Replace('"','&quot;')
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;')
}
function Resolve-QueryValue {
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# skd-compile v1.22 — Compile 1C DCS from JSON
# skd-compile v1.23 — Compile 1C DCS from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -10,7 +10,7 @@ import uuid
def esc_xml(s):
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
def fmt_dec(v):
"""Format decimal: 30.0 → '30', 16.625 → '16.625' (match PS1 output)."""
+245 -94
View File
@@ -1,4 +1,4 @@
# skd-edit v1.18 — Atomic 1C DCS editor
# skd-edit v1.22 — Atomic 1C DCS editor
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -28,6 +28,10 @@ param(
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# Dirty flag — set to $true by every successful mutation. If still $false at save time,
# the file is left untouched (NO-OP operations like [WARN] not found don't rewrite).
$script:Dirty = $false
# --- 1. Resolve path ---
if (-not $TemplatePath.EndsWith(".xml")) {
@@ -46,7 +50,7 @@ $resolvedPath = (Resolve-Path $TemplatePath).Path
function Esc-Xml {
param([string]$s)
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;').Replace('"','&quot;')
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;')
}
function Resolve-QueryValue {
@@ -179,6 +183,8 @@ function Read-FieldProperties($fieldEl) {
$props = @{
dataPath = ""; field = ""; title = ""; type = ""
roles = @(); restrict = @()
_rawTitle = $null
_unknownChildren = @()
}
foreach ($ch in $fieldEl.ChildNodes) {
@@ -187,19 +193,35 @@ function Read-FieldProperties($fieldEl) {
"dataPath" { $props.dataPath = $ch.InnerText.Trim() }
"field" { $props.field = $ch.InnerText.Trim() }
"title" {
# Extract text from LocalStringType
# Preserve full multi-lang title OuterXml — used to keep en/uk/etc.
# siblings when shorthand overrides only the ru content. Strip xmlns
# redeclarations that OuterXml adds for sub-elements.
$raw = $ch.OuterXml
$raw = [regex]::Replace($raw, ' xmlns(?::\w+)?="[^"]*"', '')
$props._rawTitle = $raw
# Also extract ru content as plain string (backward compat — used by
# external consumers reading $existing.title).
foreach ($item in $ch.ChildNodes) {
if ($item.NodeType -eq 'Element' -and $item.LocalName -eq 'item') {
$lang = $null; $content = $null
foreach ($gc in $item.ChildNodes) {
if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'content') {
$props.title = $gc.InnerText.Trim()
}
if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'lang') { $lang = $gc.InnerText.Trim() }
if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'content') { $content = $gc.InnerText.Trim() }
}
if ($lang -eq 'ru' -and $null -ne $content) { $props.title = $content }
}
}
}
"valueType" {
# Read type info — store the raw element for now, we'll use type from parsed if overridden
# Preserve the entire <valueType> OuterXml so rebuild can re-emit qualifiers
# (StringQualifiers, NumberQualifiers, DateQualifiers, etc.) that would
# otherwise be lost. Also extract Type string for type-override shorthand.
$raw = $ch.OuterXml
# .NET OuterXml re-declares xmlns on every element where the prefix is in
# scope (because the fragment is treated as standalone). Strip these since
# the parent context at insertion point already provides them.
$raw = [regex]::Replace($raw, ' xmlns(?::\w+)?="[^"]*"', '')
$props["_rawValueType"] = $raw
$typeEl = $null
foreach ($gc in $ch.ChildNodes) {
if ($gc.NodeType -eq 'Element' -and $gc.LocalName -eq 'Type') {
@@ -230,6 +252,13 @@ function Read-FieldProperties($fieldEl) {
}
}
}
default {
# Defense in depth: preserve OuterXml of unknown children so rebuild
# doesn't silently drop them (custom <editFormat>, <appearance>, etc.).
$raw = $ch.OuterXml
$raw = [regex]::Replace($raw, ' xmlns(?::\w+)?="[^"]*"', '')
$props._unknownChildren += $raw
}
}
}
return $props
@@ -723,7 +752,7 @@ function Build-ValueTypeXml {
if ($typeStr -eq "boolean") {
$lines += "$indent<v8:Type>xs:boolean</v8:Type>"
return $lines -join "`r`n"
return $lines -join "`n"
}
if ($typeStr -match '^string(\((\d+)\))?$') {
@@ -733,7 +762,7 @@ function Build-ValueTypeXml {
$lines += "$indent`t<v8:Length>$len</v8:Length>"
$lines += "$indent`t<v8:AllowedLength>Variable</v8:AllowedLength>"
$lines += "$indent</v8:StringQualifiers>"
return $lines -join "`r`n"
return $lines -join "`n"
}
if ($typeStr -match '^decimal\((\d+),(\d+)(,nonneg)?\)$') {
@@ -746,7 +775,7 @@ function Build-ValueTypeXml {
$lines += "$indent`t<v8:FractionDigits>$fraction</v8:FractionDigits>"
$lines += "$indent`t<v8:AllowedSign>$sign</v8:AllowedSign>"
$lines += "$indent</v8:NumberQualifiers>"
return $lines -join "`r`n"
return $lines -join "`n"
}
if ($typeStr -match '^(date|dateTime)$') {
@@ -758,26 +787,26 @@ function Build-ValueTypeXml {
$lines += "$indent<v8:DateQualifiers>"
$lines += "$indent`t<v8:DateFractions>$fractions</v8:DateFractions>"
$lines += "$indent</v8:DateQualifiers>"
return $lines -join "`r`n"
return $lines -join "`n"
}
if ($typeStr -eq "StandardPeriod") {
$lines += "$indent<v8:Type>v8:StandardPeriod</v8:Type>"
return $lines -join "`r`n"
return $lines -join "`n"
}
if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef)\.') {
$lines += "$indent<v8:Type xmlns:d5p1=`"http://v8.1c.ru/8.1/data/enterprise/current-config`">d5p1:$(Esc-Xml $typeStr)</v8:Type>"
return $lines -join "`r`n"
return $lines -join "`n"
}
if ($typeStr.Contains('.')) {
$lines += "$indent<v8:Type xmlns:d5p1=`"http://v8.1c.ru/8.1/data/enterprise/current-config`">d5p1:$(Esc-Xml $typeStr)</v8:Type>"
return $lines -join "`r`n"
return $lines -join "`n"
}
$lines += "$indent<v8:Type>$(Esc-Xml $typeStr)</v8:Type>"
return $lines -join "`r`n"
return $lines -join "`n"
}
function Build-MLTextXml {
@@ -789,7 +818,29 @@ function Build-MLTextXml {
$lines += "$indent`t`t<v8:content>$(Esc-Xml $text)</v8:content>"
$lines += "$indent`t</v8:item>"
$lines += "$indent</$tag>"
return $lines -join "`r`n"
return $lines -join "`n"
}
# Patches the ru <v8:content> within an existing multi-lang title OuterXml, preserving
# en/uk/etc. siblings. Used when modify-* operates with a title-override shorthand on a
# field/parameter that already has multi-language titles (typical in ERP/БП/ЗУП).
# If no ru item exists, one is prepended before the first existing item.
function Patch-MLTextRu {
param([string]$rawOuterXml, [string]$newRuText, [string]$indent)
$escaped = Esc-Xml $newRuText
$ruItemPat = '(<v8:item>\s*<v8:lang>ru</v8:lang>\s*<v8:content>)[^<]*(</v8:content>\s*</v8:item>)'
if ([regex]::IsMatch($rawOuterXml, $ruItemPat)) {
return [regex]::Replace($rawOuterXml, $ruItemPat, { param($m) $m.Groups[1].Value + $escaped + $m.Groups[2].Value })
}
# No ru item — prepend one inside the title element, before first <v8:item>.
$prep = "$indent`t<v8:item>`n$indent`t`t<v8:lang>ru</v8:lang>`n$indent`t`t<v8:content>$escaped</v8:content>`n$indent`t</v8:item>"
if ($rawOuterXml -match '<v8:item>') {
$re = New-Object System.Text.RegularExpressions.Regex('(\s*)<v8:item>')
return $re.Replace($rawOuterXml, "`n$prep`$1<v8:item>", 1)
}
# Empty title — inject after opening tag.
$re2 = New-Object System.Text.RegularExpressions.Regex('(<(?:\w+:)?title[^>]*>)')
return $re2.Replace($rawOuterXml, "`$1`n$prep`n$indent", 1)
}
function Build-RoleXml {
@@ -808,7 +859,7 @@ function Build-RoleXml {
}
}
$lines += "$indent</role>"
return $lines -join "`r`n"
return $lines -join "`n"
}
function Build-RestrictionXml {
@@ -830,7 +881,7 @@ function Build-RestrictionXml {
}
}
$lines += "$indent</useRestriction>"
return $lines -join "`r`n"
return $lines -join "`n"
}
function Build-FieldFragment {
@@ -842,7 +893,16 @@ function Build-FieldFragment {
$lines += "$i`t<dataPath>$(Esc-Xml $parsed.dataPath)</dataPath>"
$lines += "$i`t<field>$(Esc-Xml $parsed.field)</field>"
if ($parsed.title) {
# Title: prefer raw multi-lang title (preserves en/uk/etc.). When shorthand provides
# a new ru text, patch ru content inside the raw title; otherwise emit raw as-is.
# When no raw title exists, fall back to ru-only build from shorthand.
if ($parsed._rawTitle) {
if ($parsed.title -and $parsed.title -ne $parsed._existingTitleRu) {
$lines += "$i`t" + (Patch-MLTextRu $parsed._rawTitle $parsed.title "$i`t")
} else {
$lines += "$i`t" + $parsed._rawTitle
}
} elseif ($parsed.title) {
$lines += (Build-MLTextXml -tag "title" -text $parsed.title -indent "$i`t")
}
@@ -853,14 +913,26 @@ function Build-FieldFragment {
$roleXml = Build-RoleXml -roles $parsed.roles -indent "$i`t"
if ($roleXml) { $lines += $roleXml }
if ($parsed.type) {
if ($parsed.rawValueType) {
# Preserve original <valueType> verbatim — keeps qualifiers (StringQualifiers,
# NumberQualifiers, DateQualifiers, …) that aren't expressible via shorthand.
$lines += "$i`t" + $parsed.rawValueType
} elseif ($parsed.type) {
$lines += "$i`t<valueType>"
$lines += (Build-ValueTypeXml -typeStr $parsed.type -indent "$i`t`t")
$lines += "$i`t</valueType>"
}
# Defense in depth: re-emit OuterXml of unknown children (e.g. <editFormat>,
# <appearance>, custom extensions) that Read-FieldProperties captured.
if ($parsed._unknownChildren) {
foreach ($raw in $parsed._unknownChildren) {
$lines += "$i`t" + $raw
}
}
$lines += "$i</field>"
return $lines -join "`r`n"
return $lines -join "`n"
}
function Build-TotalFragment {
@@ -872,7 +944,7 @@ function Build-TotalFragment {
$lines += "$i`t<dataPath>$(Esc-Xml $parsed.dataPath)</dataPath>"
$lines += "$i`t<expression>$(Esc-Xml $parsed.expression)</expression>"
$lines += "$i</totalField>"
return $lines -join "`r`n"
return $lines -join "`n"
}
function Build-CalcFieldFragment {
@@ -899,7 +971,7 @@ function Build-CalcFieldFragment {
}
$lines += "$i</calculatedField>"
return $lines -join "`r`n"
return $lines -join "`n"
}
function Build-ParamValueXml {
@@ -1006,7 +1078,7 @@ function Build-ParamFragment {
}
$lines += "$i</parameter>"
$fragments += ($lines -join "`r`n")
$fragments += ($lines -join "`n")
if ($parsed.autoDates) {
$paramName = $parsed.name
@@ -1023,7 +1095,7 @@ function Build-ParamFragment {
$bLines += "$i`t<useRestriction>true</useRestriction>"
$bLines += "$i`t<expression>$(Esc-Xml "&$paramName.ДатаНачала")</expression>"
$bLines += "$i</parameter>"
$fragments += ($bLines -join "`r`n")
$fragments += ($bLines -join "`n")
$eLines = @()
$eLines += "$i<parameter>"
@@ -1036,7 +1108,7 @@ function Build-ParamFragment {
$eLines += "$i`t<useRestriction>true</useRestriction>"
$eLines += "$i`t<expression>$(Esc-Xml "&$paramName.ДатаОкончания")</expression>"
$eLines += "$i</parameter>"
$fragments += ($eLines -join "`r`n")
$fragments += ($eLines -join "`n")
}
return ,$fragments
@@ -1071,7 +1143,7 @@ function Build-FilterItemFragment {
}
$lines += "$i</dcsset:item>"
return $lines -join "`r`n"
return $lines -join "`n"
}
function Build-SelectionItemFragment {
@@ -1112,7 +1184,7 @@ function Build-SelectionItemFragment {
$lines += "$i`t<dcsset:field>$(Esc-Xml $fieldName)</dcsset:field>"
$lines += "$i</dcsset:item>"
}
return $lines -join "`r`n"
return $lines -join "`n"
}
function Build-DataParamFragment {
@@ -1154,7 +1226,7 @@ function Build-DataParamFragment {
}
$lines += "$i</dcscor:item>"
return $lines -join "`r`n"
return $lines -join "`n"
}
function Build-OrderItemFragment {
@@ -1170,7 +1242,7 @@ function Build-OrderItemFragment {
$lines += "$i`t<dcsset:orderType>$($parsed.direction)</dcsset:orderType>"
$lines += "$i</dcsset:item>"
}
return $lines -join "`r`n"
return $lines -join "`n"
}
function Build-DataSetLinkFragment {
@@ -1187,7 +1259,7 @@ function Build-DataSetLinkFragment {
$lines += "$i`t<parameter>$(Esc-Xml $parsed.parameter)</parameter>"
}
$lines += "$i</dataSetLink>"
return $lines -join "`r`n"
return $lines -join "`n"
}
function Build-DataSetQueryFragment {
@@ -1200,7 +1272,7 @@ function Build-DataSetQueryFragment {
$lines += "$i`t<dataSource>$(Esc-Xml $parsed.dataSource)</dataSource>"
$lines += "$i`t<query>$(Esc-Xml $parsed.query)</query>"
$lines += "$i</dataSet>"
return $lines -join "`r`n"
return $lines -join "`n"
}
function Build-VariantFragment {
@@ -1226,7 +1298,7 @@ function Build-VariantFragment {
$lines += "$i`t`t</dcsset:item>"
$lines += "$i`t</dcsset:settings>"
$lines += "$i</settingsVariant>"
return $lines -join "`r`n"
return $lines -join "`n"
}
function Emit-FilterComparison {
@@ -1308,7 +1380,7 @@ function Build-ConditionalAppearanceItemFragment {
$lines += "$i`t</dcsset:appearance>"
$lines += "$i</dcsset:item>"
return $lines -join "`r`n"
return $lines -join "`n"
}
function Build-StructureItemFragment {
@@ -1360,7 +1432,7 @@ function Build-StructureItemFragment {
}
$lines += "$i</dcsset:item>"
return $lines -join "`r`n"
return $lines -join "`n"
}
function Build-OutputParamFragment {
@@ -1388,7 +1460,7 @@ function Build-OutputParamFragment {
}
$lines += "$i</dcscor:item>"
return $lines -join "`r`n"
return $lines -join "`n"
}
# --- 5. XML helpers ---
@@ -1433,7 +1505,9 @@ function Get-ChildIndent($container) {
}
function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) {
$ws = $xmlDoc.CreateWhitespace("`r`n$childIndent")
# LF line endings — 1С DCS files use LF consistently; CRLF causes idempotency
# leaks when modify-* removes one whitespace and inserts a different-style one.
$ws = $xmlDoc.CreateWhitespace("`n$childIndent")
if ($refNode) {
$container.InsertBefore($ws, $refNode) | Out-Null
$container.InsertBefore($newNode, $ws) | Out-Null
@@ -1446,7 +1520,7 @@ function Insert-BeforeElement($container, $newNode, $refNode, $childIndent) {
$container.AppendChild($ws) | Out-Null
$container.AppendChild($newNode) | Out-Null
$parentIndent = if ($childIndent.Length -gt 1) { $childIndent.Substring(0, $childIndent.Length - 1) } else { "" }
$closeWs = $xmlDoc.CreateWhitespace("`r`n$parentIndent")
$closeWs = $xmlDoc.CreateWhitespace("`n$parentIndent")
$container.AppendChild($closeWs) | Out-Null
}
}
@@ -1717,6 +1791,18 @@ function Get-ContainerChildIndent($container) {
# --- 6. Load XML ---
# Capture raw original BEFORE DOM parse — needed at save time to:
# (a) restore exact root <DataCompositionSchema xmlns=...> opening tag (DOM serializer
# collapses multi-line xmlns into a single line);
# (b) detect NO-OP via byte-equality as an extra safety net.
$script:RawOriginal = [System.IO.File]::ReadAllText($resolvedPath, [System.Text.Encoding]::UTF8)
$rootOpenMatch = [regex]::Match($script:RawOriginal, '<DataCompositionSchema\b[^>]*>')
if ($rootOpenMatch.Success) { $script:RawRootOpening = $rootOpenMatch.Value } else { $script:RawRootOpening = $null }
# Detect line ending convention so save can normalize back to whatever the source used.
# 1С Designer writes CRLF on Windows; LF-edited files should stay LF.
$script:LineEnding = if ($script:RawOriginal.Contains("`r`n")) { "`r`n" } else { "`n" }
$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $true
$xmlDoc.Load($resolvedPath)
@@ -1767,7 +1853,7 @@ switch ($Operation) {
Insert-BeforeElement $dsNode $node $refNode $childIndent
}
Write-Host "[OK] Field `"$($parsed.dataPath)`" added to dataset `"$dsName`""
$script:Dirty = $true; Write-Host "[OK] Field `"$($parsed.dataPath)`" added to dataset `"$dsName`""
if (-not $NoSelection) {
$settings = Resolve-VariantSettings
@@ -1783,7 +1869,7 @@ switch ($Operation) {
foreach ($node in $selNodes) {
Insert-BeforeElement $selection $node $null $selIndent
}
Write-Host "[OK] Field `"$($parsed.dataPath)`" added to selection of variant `"$varName`""
$script:Dirty = $true; Write-Host "[OK] Field `"$($parsed.dataPath)`" added to selection of variant `"$varName`""
}
}
}
@@ -1819,7 +1905,7 @@ switch ($Operation) {
Insert-BeforeElement $root $node $refNode $childIndent
}
Write-Host "[OK] TotalField `"$($parsed.dataPath)`" = $($parsed.expression) added"
$script:Dirty = $true; Write-Host "[OK] TotalField `"$($parsed.dataPath)`" = $($parsed.expression) added"
}
}
@@ -1853,7 +1939,7 @@ switch ($Operation) {
Insert-BeforeElement $root $node $refNode $childIndent
}
Write-Host "[OK] CalculatedField `"$($parsed.dataPath)`" = $($parsed.expression) added"
$script:Dirty = $true; Write-Host "[OK] CalculatedField `"$($parsed.dataPath)`" = $($parsed.expression) added"
if (-not $NoSelection) {
$settings = Resolve-VariantSettings
@@ -1869,7 +1955,7 @@ switch ($Operation) {
foreach ($node in $selNodes) {
Insert-BeforeElement $selection $node $null $selIndent
}
Write-Host "[OK] Field `"$($parsed.dataPath)`" added to selection of variant `"$varName`""
$script:Dirty = $true; Write-Host "[OK] Field `"$($parsed.dataPath)`" added to selection of variant `"$varName`""
}
}
}
@@ -1907,9 +1993,9 @@ switch ($Operation) {
}
}
Write-Host "[OK] Parameter `"$($parsed.name)`" added"
$script:Dirty = $true; Write-Host "[OK] Parameter `"$($parsed.name)`" added"
if ($parsed.autoDates) {
Write-Host "[OK] Auto-parameters `"ДатаНачала`", `"ДатаОкончания`" added"
$script:Dirty = $true; Write-Host "[OK] Auto-parameters `"ДатаНачала`", `"ДатаОкончания`" added"
}
}
}
@@ -1951,9 +2037,23 @@ switch ($Operation) {
$existingTitle = $ch; break
}
}
# If the existing title has multiple <v8:item> (multi-language: ru + en + …),
# patch only the ru <v8:content> via raw-string surgery to preserve other langs.
# Otherwise rebuild as ru-only fragment.
$titleFrag = $null
if ($existingTitle) {
$rawTitle = $existingTitle.OuterXml
$rawTitle = [regex]::Replace($rawTitle, ' xmlns(?::\w+)?="[^"]*"', '')
# Count <v8:item> occurrences — if >1, treat as multi-lang.
$itemCount = ([regex]::Matches($rawTitle, '<v8:item>')).Count
if ($itemCount -gt 1) {
$titleFrag = $childIndent + (Patch-MLTextRu $rawTitle $titleVal $childIndent)
}
Remove-NodeWithWhitespace $existingTitle
}
if (-not $titleFrag) {
$titleFrag = Build-MLTextXml -tag "title" -text $titleVal -indent $childIndent
}
# Insert before first of (valueType, value, useRestriction, expression, availableAsField, ...)
$titleRef = $null
foreach ($ch in $paramEl.ChildNodes) {
@@ -1961,12 +2061,11 @@ switch ($Operation) {
$titleRef = $ch; break
}
}
$titleFrag = Build-MLTextXml -tag "title" -text $titleVal -indent $childIndent
$titleNodes = Import-Fragment $xmlDoc $titleFrag
foreach ($node in $titleNodes) {
Insert-BeforeElement $paramEl $node $titleRef $childIndent
}
Write-Host "[OK] Parameter `"$paramName`": title set to `"$titleVal`""
$script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": title set to `"$titleVal`""
}
# Separate availableValue=... from simple kv pairs
@@ -2009,7 +2108,7 @@ switch ($Operation) {
}
}
$valueLines = Build-ParamValueXml -type $declaredType -value $value -indent $childIndent
$fragXml = $valueLines -join "`r`n"
$fragXml = $valueLines -join "`n"
$wasExisting = ($null -ne $existing)
if ($existing) {
@@ -2033,10 +2132,10 @@ switch ($Operation) {
Insert-BeforeElement $paramEl $node $refNode $childIndent
}
$verb = if ($wasExisting) { "updated" } else { "added" }
Write-Host "[OK] Parameter `"$paramName`": value $verb to $value"
$script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": value $verb to $value"
} elseif ($existing) {
$existing.InnerText = $value
Write-Host "[OK] Parameter `"$paramName`": $key updated to $value"
$script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": $key updated to $value"
} else {
# Schema order: ...value, useRestriction, availableValue*, denyIncompleteValues, use
$refNode = $null
@@ -2052,7 +2151,7 @@ switch ($Operation) {
foreach ($node in $nodes) {
Insert-BeforeElement $paramEl $node $refNode $childIndent
}
Write-Host "[OK] Parameter `"$paramName`": $key=$value added"
$script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": $key=$value added"
}
}
}
@@ -2095,13 +2194,13 @@ switch ($Operation) {
}
foreach ($av in $avItems) {
$avLines = Build-AvailableValueFragment -item $av -declaredType $declaredType -indent $childIndent
$fragXml = $avLines -join "`r`n"
$fragXml = $avLines -join "`n"
$nodes = Import-Fragment $xmlDoc $fragXml
foreach ($node in $nodes) {
Insert-BeforeElement $paramEl $node $refNode $childIndent
}
}
Write-Host "[OK] Parameter `"$paramName`": availableValue set to $($avItems.Count) item(s)"
$script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": availableValue set to $($avItems.Count) item(s)"
}
# Process @hidden / @always flags (idempotent)
@@ -2138,7 +2237,7 @@ switch ($Operation) {
foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $refNode $childIndent }
}
Write-Host "[OK] Parameter `"$paramName`": @hidden applied"
$script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": @hidden applied"
}
if ($flagAlways) {
@@ -2152,7 +2251,7 @@ switch ($Operation) {
$nodes = Import-Fragment $xmlDoc "$childIndent<use>Always</use>"
foreach ($node in $nodes) { Insert-BeforeElement $paramEl $node $null $childIndent }
}
Write-Host "[OK] Parameter `"$paramName`": @always applied"
$script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`": @always applied"
}
}
}
@@ -2230,7 +2329,7 @@ switch ($Operation) {
}
}
Write-Host "[OK] Parameter renamed: `"$oldName`" => `"$newName`" (expressions updated: $exprUpdated, dataParameters updated: $dpUpdated)"
$script:Dirty = $true; Write-Host "[OK] Parameter renamed: `"$oldName`" => `"$newName`" (expressions updated: $exprUpdated, dataParameters updated: $dpUpdated)"
}
}
@@ -2307,7 +2406,7 @@ switch ($Operation) {
Insert-BeforeElement $root $pe $anchor $childIndent
}
Write-Host "[OK] Parameters reordered ($($allParams.Count) total, $($order.Count) explicit)"
$script:Dirty = $true; Write-Host "[OK] Parameters reordered ($($allParams.Count) total, $($order.Count) explicit)"
}
}
@@ -2327,7 +2426,7 @@ switch ($Operation) {
Insert-BeforeElement $filterEl $node $null $filterIndent
}
Write-Host "[OK] Filter `"$($parsed.field) $($parsed.op)`" added to variant `"$varName`""
$script:Dirty = $true; Write-Host "[OK] Filter `"$($parsed.field) $($parsed.op)`" added to variant `"$varName`""
}
}
@@ -2347,7 +2446,7 @@ switch ($Operation) {
Insert-BeforeElement $dpEl $node $null $dpIndent
}
Write-Host "[OK] DataParameter `"$($parsed.parameter)`" added to variant `"$varName`""
$script:Dirty = $true; Write-Host "[OK] DataParameter `"$($parsed.parameter)`" added to variant `"$varName`""
}
}
@@ -2389,7 +2488,7 @@ switch ($Operation) {
}
$desc = if ($parsed.field -eq "Auto") { "Auto" } else { "$($parsed.field) $($parsed.direction)" }
Write-Host "[OK] Order `"$desc`" added to variant `"$varName`""
$script:Dirty = $true; Write-Host "[OK] Order `"$desc`" added to variant `"$varName`""
}
}
@@ -2452,7 +2551,7 @@ switch ($Operation) {
}
$target = if ($groupName) { "group `"$groupName`"" } else { "variant `"$varName`"" }
Write-Host "[OK] Selection `"$fieldName`" added to $target"
$script:Dirty = $true; Write-Host "[OK] Selection `"$fieldName`" added to $target"
}
}
@@ -2469,7 +2568,7 @@ switch ($Operation) {
# InnerText setter handles XML escaping automatically
$queryEl.InnerText = Resolve-QueryValue $Value $script:queryBaseDir
Write-Host "[OK] Query replaced in dataset `"$dsName`""
$script:Dirty = $true; Write-Host "[OK] Query replaced in dataset `"$dsName`""
}
"patch-query" {
@@ -2510,7 +2609,7 @@ switch ($Operation) {
$queryEl.InnerText = $queryText.Replace($oldStr, $newStr)
$suffix = if ($once) { " (1 occurrence)" } else { " ($count occurrence(s))" }
Write-Host "[OK] Query patched in dataset `"$dsName`": replaced '$oldStr'$suffix"
$script:Dirty = $true; Write-Host "[OK] Query patched in dataset `"$dsName`": replaced '$oldStr'$suffix"
}
}
@@ -2528,9 +2627,9 @@ switch ($Operation) {
$existingParam = Find-ElementByChildValue $outputEl "item" "parameter" $parsed.key $corNs
if ($existingParam) {
Remove-NodeWithWhitespace $existingParam
Write-Host "[OK] Replaced outputParameter `"$($parsed.key)`" in variant `"$varName`""
$script:Dirty = $true; Write-Host "[OK] Replaced outputParameter `"$($parsed.key)`" in variant `"$varName`""
} else {
Write-Host "[OK] OutputParameter `"$($parsed.key)`" added to variant `"$varName`""
$script:Dirty = $true; Write-Host "[OK] OutputParameter `"$($parsed.key)`" added to variant `"$varName`""
}
$fragXml = Build-OutputParamFragment -parsed $parsed -indent $outputIndent
@@ -2572,7 +2671,7 @@ switch ($Operation) {
}
}
Write-Host "[OK] Structure set in variant `"$varName`": $Value"
$script:Dirty = $true; Write-Host "[OK] Structure set in variant `"$varName`": $Value"
}
"modify-structure" {
@@ -2659,7 +2758,7 @@ switch ($Operation) {
$lines += "$itemIndent`t<dcsset:periodAdditionBegin xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</dcsset:periodAdditionBegin>"
$lines += "$itemIndent`t<dcsset:periodAdditionEnd xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</dcsset:periodAdditionEnd>"
$lines += "$itemIndent</dcsset:item>"
$fragXml = $lines -join "`r`n"
$fragXml = $lines -join "`n"
$nodes = Import-Fragment $xmlDoc $fragXml
foreach ($node in $nodes) {
Insert-BeforeElement $giEl $node $null $itemIndent
@@ -2667,7 +2766,7 @@ switch ($Operation) {
}
$desc = if ($t.groupBy.Count -eq 0) { "details" } else { $t.groupBy -join ', ' }
Write-Host "[OK] Group `"$($t.name)`" groupItems updated: $desc"
$script:Dirty = $true; Write-Host "[OK] Group `"$($t.name)`" groupItems updated: $desc"
}
}
@@ -2697,7 +2796,7 @@ switch ($Operation) {
$desc = "$($parsed.source) > $($parsed.dest) on $($parsed.sourceExpr) = $($parsed.destExpr)"
if ($parsed.parameter) { $desc += " [param $($parsed.parameter)]" }
Write-Host "[OK] DataSetLink `"$desc`" added"
$script:Dirty = $true; Write-Host "[OK] DataSetLink `"$desc`" added"
}
}
@@ -2749,7 +2848,7 @@ switch ($Operation) {
Insert-BeforeElement $root $node $refNode $childIndent
}
Write-Host "[OK] DataSet `"$($parsed.name)`" added (dataSource=$dsSourceName)"
$script:Dirty = $true; Write-Host "[OK] DataSet `"$($parsed.name)`" added (dataSource=$dsSourceName)"
}
}
@@ -2795,7 +2894,7 @@ switch ($Operation) {
Insert-BeforeElement $root $node $refNode $childIndent
}
Write-Host "[OK] Variant `"$($parsed.name)`" [`"$($parsed.presentation)`"] added"
$script:Dirty = $true; Write-Host "[OK] Variant `"$($parsed.name)`" [`"$($parsed.presentation)`"] added"
}
}
@@ -2818,7 +2917,7 @@ switch ($Operation) {
$desc = "$($parsed.param) = $($parsed.value)"
if ($parsed.filter) { $desc += " when $($parsed.filter.field) $($parsed.filter.op)" }
if ($parsed.fields -and $parsed.fields.Count -gt 0) { $desc += " for $($parsed.fields -join ', ')" }
Write-Host "[OK] ConditionalAppearance `"$desc`" added to variant `"$varName`""
$script:Dirty = $true; Write-Host "[OK] ConditionalAppearance `"$desc`" added to variant `"$varName`""
}
}
@@ -2828,7 +2927,7 @@ switch ($Operation) {
$selection = Find-FirstElement $settings @("selection") $setNs
if ($selection) {
Clear-ContainerChildren $selection
Write-Host "[OK] Selection cleared in variant `"$varName`""
$script:Dirty = $true; Write-Host "[OK] Selection cleared in variant `"$varName`""
} else {
Write-Host "[INFO] No selection section in variant `"$varName`""
}
@@ -2840,7 +2939,7 @@ switch ($Operation) {
$orderEl = Find-FirstElement $settings @("order") $setNs
if ($orderEl) {
Clear-ContainerChildren $orderEl
Write-Host "[OK] Order cleared in variant `"$varName`""
$script:Dirty = $true; Write-Host "[OK] Order cleared in variant `"$varName`""
} else {
Write-Host "[INFO] No order section in variant `"$varName`""
}
@@ -2852,7 +2951,7 @@ switch ($Operation) {
$filterEl = Find-FirstElement $settings @("filter") $setNs
if ($filterEl) {
Clear-ContainerChildren $filterEl
Write-Host "[OK] Filter cleared in variant `"$varName`""
$script:Dirty = $true; Write-Host "[OK] Filter cleared in variant `"$varName`""
} else {
Write-Host "[INFO] No filter section in variant `"$varName`""
}
@@ -2864,7 +2963,7 @@ switch ($Operation) {
$caEl = Find-FirstElement $settings @("conditionalAppearance") $setNs
if ($caEl) {
Clear-ContainerChildren $caEl
Write-Host "[OK] ConditionalAppearance cleared in variant `"$varName`""
$script:Dirty = $true; Write-Host "[OK] ConditionalAppearance cleared in variant `"$varName`""
} else {
Write-Host "[INFO] No conditionalAppearance section in variant `"$varName`""
}
@@ -2927,7 +3026,7 @@ switch ($Operation) {
Set-OrCreateChildElement $filterItem "userSettingID" $setNs $uid $itemIndent
}
Write-Host "[OK] Filter `"$($parsed.field)`" modified in variant `"$varName`""
$script:Dirty = $true; Write-Host "[OK] Filter `"$($parsed.field)`" modified in variant `"$varName`""
}
}
@@ -2980,7 +3079,7 @@ switch ($Operation) {
} else {
$valLines += "$itemIndent<dcscor:value xsi:type=`"xs:string`">$(Esc-Xml "$($parsed.value)")</dcscor:value>"
}
$valXml = $valLines -join "`r`n"
$valXml = $valLines -join "`n"
$valNodes = Import-Fragment $xmlDoc $valXml
foreach ($node in $valNodes) {
Insert-BeforeElement $dpItem $node $null $itemIndent
@@ -3014,7 +3113,7 @@ switch ($Operation) {
Set-OrCreateChildElement $dpItem "userSettingID" $setNs $uid $itemIndent
}
Write-Host "[OK] DataParameter `"$($parsed.parameter)`" modified in variant `"$varName`""
$script:Dirty = $true; Write-Host "[OK] DataParameter `"$($parsed.parameter)`" modified in variant `"$varName`""
}
}
@@ -3044,6 +3143,14 @@ switch ($Operation) {
type = if ($parsed.type) { $parsed.type } else { $existing.type }
roles = if ($parsed.roles -and $parsed.roles.Count -gt 0) { $parsed.roles } else { $existing.roles }
restrict = if ($parsed.restrict -and $parsed.restrict.Count -gt 0) { $parsed.restrict } else { $existing.restrict }
# Preserve raw <valueType> only when user did NOT override type via shorthand —
# otherwise the override path rebuilds valueType from $parsed.type.
rawValueType = if ($parsed.type) { $null } else { $existing._rawValueType }
# Preserve raw multi-lang title; pass existing ru content for change detection.
_rawTitle = $existing._rawTitle
_existingTitleRu = $existing.title
# Pass-through unknown children (e.g. <editFormat>, <appearance>, custom extensions).
_unknownChildren = $existing._unknownChildren
}
# Remember position (NextSibling after whitespace)
@@ -3065,7 +3172,7 @@ switch ($Operation) {
Insert-BeforeElement $dsNode $node $nextSib $childIndent
}
Write-Host "[OK] Field `"$fieldName`" modified in dataset `"$dsName`""
$script:Dirty = $true; Write-Host "[OK] Field `"$fieldName`" modified in dataset `"$dsName`""
}
}
@@ -3103,16 +3210,32 @@ switch ($Operation) {
$fieldIndent = Get-ChildIndent $fieldEl
# Remove existing <role>
# Remove existing <role> — but first capture OuterXml of any sub-children that
# Build-RoleXml won't re-emit (e.g. <dcscom:addition>, <dcscom:groupFields>,
# custom extension elements). Preserved across rebuild.
$oldRole = $null
foreach ($ch in $fieldEl.ChildNodes) {
if ($ch.NodeType -eq 'Element' -and $ch.LocalName -eq 'role' -and $ch.NamespaceURI -eq $schNs) { $oldRole = $ch; break }
}
if ($oldRole) { Remove-NodeWithWhitespace $oldRole }
$knownRoleChildren = @('periodNumber','periodType','dimension','ignoreNullsInGroups','balance','account','accountTypeExpression','additionType','addition')
$preservedRoleChildren = @()
if ($oldRole) {
foreach ($gc in $oldRole.ChildNodes) {
if ($gc.NodeType -ne 'Element') { continue }
if ($knownRoleChildren -contains $gc.LocalName) { continue }
# kv keys override the same-named sub-element on rebuild — don't preserve
# what the user explicitly set.
if ($kv.Contains($gc.LocalName)) { continue }
$raw = $gc.OuterXml
$raw = [regex]::Replace($raw, ' xmlns(?::\w+)?="[^"]*"', '')
$preservedRoleChildren += $raw
}
Remove-NodeWithWhitespace $oldRole
}
# Empty spec — remove only
if ($flags.Count -eq 0 -and $kv.Count -eq 0) {
Write-Host "[OK] Field `"$dataPath`" role cleared"
$script:Dirty = $true; Write-Host "[OK] Field `"$dataPath`" role cleared"
continue
}
@@ -3130,8 +3253,11 @@ switch ($Operation) {
foreach ($k in $kv.Keys) {
$lines += "$fieldIndent`t<dcscom:$k>$(Esc-Xml $kv[$k])</dcscom:$k>"
}
foreach ($raw in $preservedRoleChildren) {
$lines += "$fieldIndent`t" + $raw
}
$lines += "$fieldIndent</role>"
$fragXml = $lines -join "`r`n"
$fragXml = $lines -join "`n"
# Insert before <valueType>, else before <inputParameters>, else at end
$refNode = $null
@@ -3146,7 +3272,7 @@ switch ($Operation) {
$desc = @()
if ($flags.Count -gt 0) { $desc += ($flags | ForEach-Object { "@$_" }) -join ' ' }
if ($kv.Count -gt 0) { $desc += ($kv.Keys | ForEach-Object { "$_=$($kv[$_])" }) -join ' ' }
Write-Host "[OK] Field `"$dataPath`" role set: $($desc -join ' ')"
$script:Dirty = $true; Write-Host "[OK] Field `"$dataPath`" role set: $($desc -join ' ')"
}
}
@@ -3164,7 +3290,7 @@ switch ($Operation) {
}
Remove-NodeWithWhitespace $fieldEl
Write-Host "[OK] Field `"$fieldName`" removed from dataset `"$dsName`""
$script:Dirty = $true; Write-Host "[OK] Field `"$fieldName`" removed from dataset `"$dsName`""
# Also remove from selection in variant
try {
@@ -3175,7 +3301,7 @@ switch ($Operation) {
$selItem = Find-ElementByChildValue $selection "item" "field" $fieldName $setNs
if ($selItem) {
Remove-NodeWithWhitespace $selItem
Write-Host "[OK] Field `"$fieldName`" removed from selection of variant `"$varName`""
$script:Dirty = $true; Write-Host "[OK] Field `"$fieldName`" removed from selection of variant `"$varName`""
}
}
} catch {
@@ -3196,7 +3322,7 @@ switch ($Operation) {
}
Remove-NodeWithWhitespace $totalEl
Write-Host "[OK] TotalField `"$dataPath`" removed"
$script:Dirty = $true; Write-Host "[OK] TotalField `"$dataPath`" removed"
}
}
@@ -3212,7 +3338,7 @@ switch ($Operation) {
}
Remove-NodeWithWhitespace $calcEl
Write-Host "[OK] CalculatedField `"$dataPath`" removed"
$script:Dirty = $true; Write-Host "[OK] CalculatedField `"$dataPath`" removed"
# Also remove from selection
try {
@@ -3223,7 +3349,7 @@ switch ($Operation) {
$selItem = Find-ElementByChildValue $selection "item" "field" $dataPath $setNs
if ($selItem) {
Remove-NodeWithWhitespace $selItem
Write-Host "[OK] Field `"$dataPath`" removed from selection of variant `"$varName`""
$script:Dirty = $true; Write-Host "[OK] Field `"$dataPath`" removed from selection of variant `"$varName`""
}
}
} catch { }
@@ -3242,7 +3368,7 @@ switch ($Operation) {
}
Remove-NodeWithWhitespace $paramEl
Write-Host "[OK] Parameter `"$paramName`" removed"
$script:Dirty = $true; Write-Host "[OK] Parameter `"$paramName`" removed"
}
}
@@ -3266,7 +3392,7 @@ switch ($Operation) {
}
Remove-NodeWithWhitespace $filterItem
Write-Host "[OK] Filter for `"$fieldName`" removed from variant `"$varName`""
$script:Dirty = $true; Write-Host "[OK] Filter for `"$fieldName`" removed from variant `"$varName`""
}
}
@@ -3400,7 +3526,7 @@ switch ($Operation) {
$searchStart = $cellEnd + 1
}
Write-Host "[OK] $drillName$tplName (param + $cellCount cell(s))"
$script:Dirty = $true; Write-Host "[OK] $drillName$tplName (param + $cellCount cell(s))"
}
}
@@ -3415,15 +3541,40 @@ switch ($Operation) {
# Write directly — skip DOM save
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($resolvedPath, $rawText, $enc)
Write-Host "[OK] Saved $resolvedPath"
$script:Dirty = $true; Write-Host "[OK] Saved $resolvedPath"
exit 0
}
}
# --- 9. Save ---
if (-not $script:Dirty) {
Write-Host "[INFO] No changes -- file untouched"
exit 0
}
$content = $xmlDoc.OuterXml
$content = $content -replace '(?<=<\?xml[^?]*encoding=")utf-8(?=")', 'UTF-8'
# Format-preserve post-processing:
# (1) restore the original raw <DataCompositionSchema ...> opening tag — DOM collapses
# multi-line xmlns declarations into one line.
if ($script:RawRootOpening) {
$content = [regex]::Replace($content, '<DataCompositionSchema\b[^>]*>', { param($m) $script:RawRootOpening })
}
# (2) normalize self-closing tags: `.NET XmlDocument` adds a space before `/>`
# (`<foo bar="x" />`) but 1C-Designer writes `<foo bar="x"/>`. Strip the space.
$content = [regex]::Replace($content, '(?<=\S) />', '/>')
# (3) normalize line endings to match source — operations may mix LF (from new
# fragments) with whatever the source used (CRLF on Windows, LF on Linux/git).
if ($script:LineEnding -eq "`r`n") {
$content = $content -replace '(?<!\r)\n', "`r`n"
} else {
$content = $content -replace "`r`n", "`n"
}
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($resolvedPath, $content, $enc)
+213 -89
View File
@@ -1,4 +1,4 @@
# skd-edit v1.18 — Atomic 1C DCS editor (Python port)
# skd-edit v1.22 — Atomic 1C DCS editor (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
@@ -11,6 +11,10 @@ from lxml import etree
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
# Dirty flag — set to True by every successful mutation. If still False at save time,
# the file is left untouched (NO-OP operations like [WARN] not found don't rewrite).
dirty = False
# ── arg parsing ──────────────────────────────────────────────
VALID_OPS = [
@@ -76,7 +80,7 @@ def local_name(node):
# ── helpers ──────────────────────────────────────────────────
def esc_xml(s):
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
def resolve_query_value(val, base_dir):
@@ -210,7 +214,8 @@ def parse_field_shorthand(s):
def read_field_properties(field_el):
props = {"dataPath": "", "field": "", "title": "", "type": "", "roles": [], "restrict": [], "_rawTypeText": ""}
props = {"dataPath": "", "field": "", "title": "", "type": "", "roles": [], "restrict": [], "_rawTypeText": "",
"_rawTitle": None, "_unknownChildren": []}
for ch in field_el:
if not isinstance(ch.tag, str):
@@ -221,12 +226,29 @@ def read_field_properties(field_el):
elif ln == "field":
props["field"] = (ch.text or "").strip()
elif ln == "title":
# Preserve full multi-lang title OuterXml; also extract ru content for compat.
raw = etree.tostring(ch, encoding="unicode", with_tail=False)
raw = re.sub(r' xmlns(?::\w+)?="[^"]*"', "", raw)
props["_rawTitle"] = raw
for item in ch:
if isinstance(item.tag, str) and local_name(item) == "item":
lang = None
content = None
for gc in item:
if isinstance(gc.tag, str) and local_name(gc) == "lang":
lang = (gc.text or "").strip()
if isinstance(gc.tag, str) and local_name(gc) == "content":
props["title"] = (gc.text or "").strip()
content = (gc.text or "").strip()
if lang == "ru" and content is not None:
props["title"] = content
elif ln == "valueType":
# Preserve full <valueType> serialization so rebuild can re-emit qualifiers
# (StringQualifiers, NumberQualifiers, DateQualifiers, …) that aren't
# expressible via shorthand. Strip xmlns declarations that lxml re-emits when
# serializing a sub-element (parent context already provides them).
raw = etree.tostring(ch, encoding="unicode", with_tail=False)
raw = re.sub(r' xmlns(?::\w+)?="[^"]*"', "", raw)
props["_rawValueType"] = raw
for gc in ch:
if isinstance(gc.tag, str) and local_name(gc) == "Type":
props["_rawTypeText"] = (gc.text or "").strip()
@@ -246,6 +268,12 @@ def read_field_properties(field_el):
mapped = rev_map.get(local_name(gc))
if mapped:
props["restrict"].append(mapped)
else:
# Defense in depth: preserve OuterXml of unknown children so rebuild
# doesn't silently drop them (custom <editFormat>, <appearance>, etc.).
raw = etree.tostring(ch, encoding="unicode", with_tail=False)
raw = re.sub(r' xmlns(?::\w+)?="[^"]*"', "", raw)
props["_unknownChildren"].append(raw)
return props
@@ -679,7 +707,7 @@ def build_value_type_xml(type_str, indent):
if type_str == "boolean":
lines.append(f"{indent}<v8:Type>xs:boolean</v8:Type>")
return "\r\n".join(lines)
return "\n".join(lines)
m = re.match(r'^string(\((\d+)\))?$', type_str)
if m:
@@ -689,7 +717,7 @@ def build_value_type_xml(type_str, indent):
lines.append(f"{indent}\t<v8:Length>{length}</v8:Length>")
lines.append(f"{indent}\t<v8:AllowedLength>Variable</v8:AllowedLength>")
lines.append(f"{indent}</v8:StringQualifiers>")
return "\r\n".join(lines)
return "\n".join(lines)
m = re.match(r'^decimal\((\d+),(\d+)(,nonneg)?\)$', type_str)
if m:
@@ -701,7 +729,7 @@ def build_value_type_xml(type_str, indent):
lines.append(f"{indent}\t<v8:FractionDigits>{fraction}</v8:FractionDigits>")
lines.append(f"{indent}\t<v8:AllowedSign>{sign}</v8:AllowedSign>")
lines.append(f"{indent}</v8:NumberQualifiers>")
return "\r\n".join(lines)
return "\n".join(lines)
m = re.match(r'^(date|dateTime)$', type_str)
if m:
@@ -710,22 +738,22 @@ def build_value_type_xml(type_str, indent):
lines.append(f"{indent}<v8:DateQualifiers>")
lines.append(f"{indent}\t<v8:DateFractions>{fractions}</v8:DateFractions>")
lines.append(f"{indent}</v8:DateQualifiers>")
return "\r\n".join(lines)
return "\n".join(lines)
if type_str == "StandardPeriod":
lines.append(f"{indent}<v8:Type>v8:StandardPeriod</v8:Type>")
return "\r\n".join(lines)
return "\n".join(lines)
if re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef)\.', type_str):
lines.append(f'{indent}<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:{esc_xml(type_str)}</v8:Type>')
return "\r\n".join(lines)
return "\n".join(lines)
if "." in type_str:
lines.append(f'{indent}<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:{esc_xml(type_str)}</v8:Type>')
return "\r\n".join(lines)
return "\n".join(lines)
lines.append(f"{indent}<v8:Type>{esc_xml(type_str)}</v8:Type>")
return "\r\n".join(lines)
return "\n".join(lines)
def build_mltext_xml(tag, text, indent):
@@ -737,7 +765,20 @@ def build_mltext_xml(tag, text, indent):
f"{indent}\t</v8:item>",
f"{indent}</{tag}>",
]
return "\r\n".join(lines)
return "\n".join(lines)
def patch_mltext_ru(raw_outer_xml, new_ru_text, indent):
"""Patch the ru <v8:content> within an existing multi-lang title OuterXml,
preserving en/uk/etc. siblings. Mirrors PS Patch-MLTextRu."""
escaped = esc_xml(new_ru_text)
ru_item_pat = r"(<v8:item>\s*<v8:lang>ru</v8:lang>\s*<v8:content>)[^<]*(</v8:content>\s*</v8:item>)"
if re.search(ru_item_pat, raw_outer_xml):
return re.sub(ru_item_pat, lambda m: m.group(1) + escaped + m.group(2), raw_outer_xml)
prep = f"{indent}\t<v8:item>\n{indent}\t\t<v8:lang>ru</v8:lang>\n{indent}\t\t<v8:content>{escaped}</v8:content>\n{indent}\t</v8:item>"
if "<v8:item>" in raw_outer_xml:
return re.sub(r"(\s*)<v8:item>", lambda m: "\n" + prep + m.group(1) + "<v8:item>", raw_outer_xml, count=1)
return re.sub(r"(<(?:\w+:)?title[^>]*>)", lambda m: m.group(1) + "\n" + prep + "\n" + indent, raw_outer_xml, count=1)
def build_role_xml(roles, indent):
@@ -751,7 +792,7 @@ def build_role_xml(roles, indent):
else:
lines.append(f"{indent}\t<dcscom:{role}>true</dcscom:{role}>")
lines.append(f"{indent}</role>")
return "\r\n".join(lines)
return "\n".join(lines)
def build_restriction_xml(restrict, indent):
@@ -764,7 +805,7 @@ def build_restriction_xml(restrict, indent):
if xml_name:
lines.append(f"{indent}\t<{xml_name}>true</{xml_name}>")
lines.append(f"{indent}</useRestriction>")
return "\r\n".join(lines)
return "\n".join(lines)
def build_field_fragment(parsed, indent):
@@ -773,7 +814,15 @@ def build_field_fragment(parsed, indent):
lines.append(f"{i}\t<dataPath>{esc_xml(parsed['dataPath'])}</dataPath>")
lines.append(f"{i}\t<field>{esc_xml(parsed['field'])}</field>")
if parsed.get("title"):
# Title: prefer raw multi-lang OuterXml (preserves en/uk/etc.). When shorthand
# provides a new ru text different from existing, patch the ru content. Otherwise
# emit raw as-is or build ru-only from shorthand if there was no prior title.
if parsed.get("_rawTitle"):
if parsed.get("title") and parsed["title"] != parsed.get("_existingTitleRu"):
lines.append(f"{i}\t" + patch_mltext_ru(parsed["_rawTitle"], parsed["title"], f"{i}\t"))
else:
lines.append(f"{i}\t" + parsed["_rawTitle"])
elif parsed.get("title"):
lines.append(build_mltext_xml("title", parsed["title"], f"{i}\t"))
if parsed.get("restrict"):
@@ -783,13 +832,21 @@ def build_field_fragment(parsed, indent):
if role_xml:
lines.append(role_xml)
if parsed.get("type"):
if parsed.get("rawValueType"):
# Preserve original <valueType> verbatim — keeps qualifiers (StringQualifiers,
# NumberQualifiers, DateQualifiers, …) that aren't expressible via shorthand.
lines.append(f"{i}\t" + parsed["rawValueType"])
elif parsed.get("type"):
lines.append(f"{i}\t<valueType>")
lines.append(build_value_type_xml(parsed["type"], f"{i}\t\t"))
lines.append(f"{i}\t</valueType>")
# Defense in depth: re-emit OuterXml of unknown children captured by Read.
for raw in (parsed.get("_unknownChildren") or []):
lines.append(f"{i}\t" + raw)
lines.append(f"{i}</field>")
return "\r\n".join(lines)
return "\n".join(lines)
def build_total_fragment(parsed, indent):
@@ -800,7 +857,7 @@ def build_total_fragment(parsed, indent):
f"{i}\t<expression>{esc_xml(parsed['expression'])}</expression>",
f"{i}</totalField>",
]
return "\r\n".join(lines)
return "\n".join(lines)
def build_calc_field_fragment(parsed, indent):
@@ -819,7 +876,7 @@ def build_calc_field_fragment(parsed, indent):
lines.append(build_value_type_xml(parsed["type"], f"{i}\t\t"))
lines.append(f"{i}\t</valueType>")
lines.append(f"{i}</calculatedField>")
return "\r\n".join(lines)
return "\n".join(lines)
def build_param_value_xml(type_str, value, indent, tag_name="value", tag_ns=""):
@@ -893,7 +950,7 @@ def build_param_fragment(parsed, indent):
lines.append(f"{i}\t<use>Always</use>")
lines.append(f"{i}</parameter>")
fragments.append("\r\n".join(lines))
fragments.append("\n".join(lines))
if parsed.get("autoDates"):
param_name = parsed["name"]
@@ -910,7 +967,7 @@ def build_param_fragment(parsed, indent):
f"{i}\t<expression>{esc_xml('&' + param_name + '.\u0414\u0430\u0442\u0430\u041d\u0430\u0447\u0430\u043b\u0430')}</expression>",
f"{i}</parameter>",
]
fragments.append("\r\n".join(b_lines))
fragments.append("\n".join(b_lines))
e_lines = [
f"{i}<parameter>",
@@ -924,7 +981,7 @@ def build_param_fragment(parsed, indent):
f"{i}\t<expression>{esc_xml('&' + param_name + '.\u0414\u0430\u0442\u0430\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f')}</expression>",
f"{i}</parameter>",
]
fragments.append("\r\n".join(e_lines))
fragments.append("\n".join(e_lines))
return fragments
@@ -951,7 +1008,7 @@ def build_filter_item_fragment(parsed, indent):
lines.append(f"{i}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>")
lines.append(f"{i}</dcsset:item>")
return "\r\n".join(lines)
return "\n".join(lines)
def build_selection_item_fragment(field_name, indent):
@@ -982,13 +1039,13 @@ def build_selection_item_fragment(field_name, indent):
lines.append(f"{i}\t</dcsset:item>")
lines.append(f"{i}\t<dcsset:placement>Auto</dcsset:placement>")
lines.append(f"{i}</dcsset:item>")
return "\r\n".join(lines)
return "\n".join(lines)
lines = [
f'{i}<dcsset:item xsi:type="dcsset:SelectedItemField">',
f"{i}\t<dcsset:field>{esc_xml(field_name)}</dcsset:field>",
f"{i}</dcsset:item>",
]
return "\r\n".join(lines)
return "\n".join(lines)
def build_data_param_fragment(parsed, indent):
@@ -1023,7 +1080,7 @@ def build_data_param_fragment(parsed, indent):
lines.append(f"{i}\t<dcsset:userSettingID>{esc_xml(uid)}</dcsset:userSettingID>")
lines.append(f"{i}</dcscor:item>")
return "\r\n".join(lines)
return "\n".join(lines)
def build_order_item_fragment(parsed, indent):
@@ -1036,7 +1093,7 @@ def build_order_item_fragment(parsed, indent):
f"{i}\t<dcsset:orderType>{parsed['direction']}</dcsset:orderType>",
f"{i}</dcsset:item>",
]
return "\r\n".join(lines)
return "\n".join(lines)
def build_data_set_link_fragment(parsed, indent):
@@ -1051,7 +1108,7 @@ def build_data_set_link_fragment(parsed, indent):
if parsed.get("parameter"):
lines.append(f"{i}\t<parameter>{esc_xml(parsed['parameter'])}</parameter>")
lines.append(f"{i}</dataSetLink>")
return "\r\n".join(lines)
return "\n".join(lines)
def build_data_set_query_fragment(parsed, indent):
@@ -1063,7 +1120,7 @@ def build_data_set_query_fragment(parsed, indent):
f"{i}\t<query>{esc_xml(parsed['query'])}</query>",
f"{i}</dataSet>",
]
return "\r\n".join(lines)
return "\n".join(lines)
def build_variant_fragment(parsed, indent):
@@ -1088,7 +1145,7 @@ def build_variant_fragment(parsed, indent):
f"{i}\t</dcsset:settings>",
f"{i}</settingsVariant>",
]
return "\r\n".join(lines)
return "\n".join(lines)
def _emit_filter_comparison(lines, f, indent):
@@ -1155,7 +1212,7 @@ def build_conditional_appearance_item_fragment(parsed, indent):
lines.append(f"{i}\t</dcsset:appearance>")
lines.append(f"{i}</dcsset:item>")
return "\r\n".join(lines)
return "\n".join(lines)
def build_structure_item_fragment(item, indent):
@@ -1192,7 +1249,7 @@ def build_structure_item_fragment(item, indent):
lines.append(build_structure_item_fragment(child, f"{i}\t"))
lines.append(f"{i}</dcsset:item>")
return "\r\n".join(lines)
return "\n".join(lines)
def build_output_param_fragment(parsed, indent):
@@ -1215,7 +1272,7 @@ def build_output_param_fragment(parsed, indent):
lines.append(f'{i}\t<dcscor:value xsi:type="{ptype}">{esc_xml(val)}</dcscor:value>')
lines.append(f"{i}</dcscor:item>")
return "\r\n".join(lines)
return "\n".join(lines)
# ── 5. XML helpers ──────────────────────────────────────────
@@ -1492,6 +1549,18 @@ def get_container_child_indent(container):
# ── 6. Load XML ─────────────────────────────────────────────
# Capture raw original BEFORE parse — needed at save time to restore exact root
# <DataCompositionSchema xmlns=...> opening tag (lxml's tostring() collapses multi-line
# xmlns into one line) and to detect NO-OP via byte-equality as a safety net.
with open(resolved_path, "rb") as _f:
raw_original_bytes = _f.read()
raw_original_text = raw_original_bytes.lstrip(b"\xef\xbb\xbf").decode("utf-8")
_root_open_m = re.search(r"<DataCompositionSchema\b[^>]*>", raw_original_text, re.DOTALL)
raw_root_opening = _root_open_m.group(0) if _root_open_m else None
# Detect line ending convention so save can normalize back to whatever the source used.
line_ending = "\r\n" if "\r\n" in raw_original_text else "\n"
xml_parser = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(resolved_path, xml_parser)
xml_doc = tree.getroot()
@@ -1532,7 +1601,7 @@ if operation == "add-field":
for node in nodes:
insert_before_element(ds_node, node, ref_node, child_indent)
print(f'[OK] Field "{parsed["dataPath"]}" added to dataset "{ds_name}"')
dirty = True; print(f'[OK] Field "{parsed["dataPath"]}" added to dataset "{ds_name}"')
if not no_selection:
settings = resolve_variant_settings()
@@ -1547,7 +1616,7 @@ if operation == "add-field":
sel_nodes = import_fragment(xml_doc, sel_xml)
for node in sel_nodes:
insert_before_element(selection, node, None, sel_indent)
print(f'[OK] Field "{parsed["dataPath"]}" added to selection of variant "{var_name}"')
dirty = True; print(f'[OK] Field "{parsed["dataPath"]}" added to selection of variant "{var_name}"')
elif operation == "add-total":
for val in values:
@@ -1579,7 +1648,7 @@ elif operation == "add-total":
for node in nodes:
insert_before_element(xml_doc, node, ref_node, child_indent)
print(f'[OK] TotalField "{parsed["dataPath"]}" = {parsed["expression"]} added')
dirty = True; print(f'[OK] TotalField "{parsed["dataPath"]}" = {parsed["expression"]} added')
elif operation == "add-calculated-field":
for val in values:
@@ -1610,7 +1679,7 @@ elif operation == "add-calculated-field":
for node in nodes:
insert_before_element(xml_doc, node, ref_node, child_indent)
print(f'[OK] CalculatedField "{parsed["dataPath"]}" = {parsed["expression"]} added')
dirty = True; print(f'[OK] CalculatedField "{parsed["dataPath"]}" = {parsed["expression"]} added')
if not no_selection:
settings = resolve_variant_settings()
@@ -1625,7 +1694,7 @@ elif operation == "add-calculated-field":
sel_nodes = import_fragment(xml_doc, sel_xml)
for node in sel_nodes:
insert_before_element(selection, node, None, sel_indent)
print(f'[OK] Field "{parsed["dataPath"]}" added to selection of variant "{var_name}"')
dirty = True; print(f'[OK] Field "{parsed["dataPath"]}" added to selection of variant "{var_name}"')
elif operation == "add-parameter":
for val in values:
@@ -1657,9 +1726,9 @@ elif operation == "add-parameter":
for node in nodes:
insert_before_element(xml_doc, node, ref_node, child_indent)
print(f'[OK] Parameter "{parsed["name"]}" added')
dirty = True; print(f'[OK] Parameter "{parsed["name"]}" added')
if parsed.get("autoDates"):
print('[OK] Auto-parameters "\u0414\u0430\u0442\u0430\u041d\u0430\u0447\u0430\u043b\u0430", "\u0414\u0430\u0442\u0430\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f" added')
dirty = True; print('[OK] Auto-parameters "\u0414\u0430\u0442\u0430\u041d\u0430\u0447\u0430\u043b\u0430", "\u0414\u0430\u0442\u0430\u041e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f" added')
elif operation == "modify-parameter":
for val in values:
@@ -1693,14 +1762,22 @@ elif operation == "modify-parameter":
# Set/replace title (must come right after <name>, before <valueType>)
if title_val is not None:
existing_title = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "title"), None)
# If existing title is multi-lang (has >1 <v8:item>), patch ru content
# while preserving other languages. Otherwise rebuild as ru-only.
title_frag = None
if existing_title is not None:
raw_title = etree.tostring(existing_title, encoding="unicode", with_tail=False)
raw_title = re.sub(r' xmlns(?::\w+)?="[^"]*"', "", raw_title)
if raw_title.count("<v8:item>") > 1:
title_frag = child_indent + patch_mltext_ru(raw_title, title_val, child_indent)
remove_node_with_whitespace(existing_title)
if title_frag is None:
title_frag = build_mltext_xml("title", title_val, child_indent)
# Insert before the first child after <name>
title_ref = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) != "name"), None)
title_frag = build_mltext_xml("title", title_val, child_indent)
for node in import_fragment(xml_doc, title_frag):
insert_before_element(param_el, node, title_ref, child_indent)
print(f'[OK] Parameter "{param_name}": title set to "{title_val}"')
dirty = True; print(f'[OK] Parameter "{param_name}": title set to "{title_val}"')
# Separate availableValue=... from simple kv pairs
simple_rest = rest
@@ -1726,7 +1803,7 @@ elif operation == "modify-parameter":
declared_type = re.sub(r'^d\d+p\d+:', '', (tnode.text or "").strip())
break
value_lines = build_param_value_xml(declared_type, value, child_indent)
frag_xml = "\r\n".join(value_lines)
frag_xml = "\n".join(value_lines)
was_existing = existing is not None
if existing is not None:
# Find next-element sibling as ref before removing
@@ -1739,10 +1816,10 @@ elif operation == "modify-parameter":
for node in nodes:
insert_before_element(param_el, node, ref_node, child_indent)
verb = "updated" if was_existing else "added"
print(f'[OK] Parameter "{param_name}": value {verb} to {value}')
dirty = True; print(f'[OK] Parameter "{param_name}": value {verb} to {value}')
elif existing is not None:
existing.text = value
print(f'[OK] Parameter "{param_name}": {key} updated to {value}')
dirty = True; print(f'[OK] Parameter "{param_name}": {key} updated to {value}')
else:
# Schema order: ...value, useRestriction, availableValue*, denyIncompleteValues, use
ref_node = None
@@ -1752,7 +1829,7 @@ elif operation == "modify-parameter":
nodes = import_fragment(xml_doc, frag_xml)
for node in nodes:
insert_before_element(param_el, node, ref_node, child_indent)
print(f'[OK] Parameter "{param_name}": {key}={value} added')
dirty = True; print(f'[OK] Parameter "{param_name}": {key}={value} added')
# Process availableValue
if av_part:
@@ -1785,11 +1862,11 @@ elif operation == "modify-parameter":
break
for av in av_items:
av_lines = build_available_value_fragment(av, declared_type, child_indent)
frag_xml = "\r\n".join(av_lines)
frag_xml = "\n".join(av_lines)
nodes = import_fragment(xml_doc, frag_xml)
for node in nodes:
insert_before_element(param_el, node, ref_node, child_indent)
print(f'[OK] Parameter "{param_name}": availableValue set to {len(av_items)} item(s)')
dirty = True; print(f'[OK] Parameter "{param_name}": availableValue set to {len(av_items)} item(s)')
if flag_hidden:
ur_el = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "useRestriction" and etree.QName(ch.tag).namespace == SCH_NS), None)
@@ -1810,7 +1887,7 @@ elif operation == "modify-parameter":
for node in import_fragment(xml_doc, f"{child_indent}<availableAsField>false</availableAsField>"):
insert_before_element(param_el, node, ref_node, child_indent)
print(f'[OK] Parameter "{param_name}": @hidden applied')
dirty = True; print(f'[OK] Parameter "{param_name}": @hidden applied')
if flag_always:
use_el = next((ch for ch in param_el if isinstance(ch.tag, str) and local_name(ch) == "use" and etree.QName(ch.tag).namespace == SCH_NS), None)
@@ -1820,7 +1897,7 @@ elif operation == "modify-parameter":
else:
for node in import_fragment(xml_doc, f"{child_indent}<use>Always</use>"):
insert_before_element(param_el, node, None, child_indent)
print(f'[OK] Parameter "{param_name}": @always applied')
dirty = True; print(f'[OK] Parameter "{param_name}": @always applied')
elif operation == "rename-parameter":
root = xml_doc
@@ -1883,7 +1960,7 @@ elif operation == "rename-parameter":
gc.text = new_name
dp_updated += 1
print(f'[OK] Parameter renamed: "{old_name}" => "{new_name}" (expressions updated: {expr_updated}, dataParameters updated: {dp_updated})')
dirty = True; print(f'[OK] Parameter renamed: "{old_name}" => "{new_name}" (expressions updated: {expr_updated}, dataParameters updated: {dp_updated})')
elif operation == "reorder-parameters":
root = xml_doc
@@ -1940,7 +2017,7 @@ elif operation == "reorder-parameters":
for pe in new_order:
insert_before_element(root, pe, anchor, child_indent)
print(f'[OK] Parameters reordered ({len(all_params)} total, {len(order)} explicit)')
dirty = True; print(f'[OK] Parameters reordered ({len(all_params)} total, {len(order)} explicit)')
elif operation == "add-filter":
settings = resolve_variant_settings()
@@ -1953,7 +2030,7 @@ elif operation == "add-filter":
nodes = import_fragment(xml_doc, frag_xml)
for node in nodes:
insert_before_element(filter_el, node, None, filter_indent)
print(f'[OK] Filter "{parsed["field"]} {parsed["op"]}" added to variant "{var_name}"')
dirty = True; print(f'[OK] Filter "{parsed["field"]} {parsed["op"]}" added to variant "{var_name}"')
elif operation == "add-dataParameter":
settings = resolve_variant_settings()
@@ -1966,7 +2043,7 @@ elif operation == "add-dataParameter":
nodes = import_fragment(xml_doc, frag_xml)
for node in nodes:
insert_before_element(dp_el, node, None, dp_indent)
print(f'[OK] DataParameter "{parsed["parameter"]}" added to variant "{var_name}"')
dirty = True; print(f'[OK] DataParameter "{parsed["parameter"]}" added to variant "{var_name}"')
elif operation == "add-order":
settings = resolve_variant_settings()
@@ -1999,7 +2076,7 @@ elif operation == "add-order":
insert_before_element(order_el, node, None, order_indent)
desc = "Auto" if parsed["field"] == "Auto" else f"{parsed['field']} {parsed['direction']}"
print(f'[OK] Order "{desc}" added to variant "{var_name}"')
dirty = True; print(f'[OK] Order "{desc}" added to variant "{var_name}"')
elif operation == "add-selection":
settings = resolve_variant_settings()
@@ -2052,7 +2129,7 @@ elif operation == "add-selection":
for node in sel_nodes:
insert_before_element(selection, node, None, sel_indent)
target = f'group "{group_name}"' if group_name else f'variant "{var_name}"'
print(f'[OK] Selection "{field_name}" added to {target}')
dirty = True; print(f'[OK] Selection "{field_name}" added to {target}')
elif operation == "set-query":
ds_node = resolve_data_set()
@@ -2062,7 +2139,7 @@ elif operation == "set-query":
print(f"No <query> element found in dataset '{ds_name}'", file=sys.stderr)
sys.exit(1)
query_el.text = resolve_query_value(value_arg, query_base_dir)
print(f'[OK] Query replaced in dataset "{ds_name}"')
dirty = True; print(f'[OK] Query replaced in dataset "{ds_name}"')
elif operation == "patch-query":
ds_node = resolve_data_set()
@@ -2095,7 +2172,7 @@ elif operation == "patch-query":
query_el.text = query_text.replace(old_str, new_str)
suffix = " (1 occurrence)" if once else f" ({count} occurrence(s))"
print(f'[OK] Query patched in dataset "{ds_name}": replaced \'{old_str}\'{suffix}')
dirty = True; print(f'[OK] Query patched in dataset "{ds_name}": replaced \'{old_str}\'{suffix}')
elif operation == "set-outputParameter":
settings = resolve_variant_settings()
@@ -2108,9 +2185,9 @@ elif operation == "set-outputParameter":
existing_param = find_element_by_child_value(output_el, "item", "parameter", parsed["key"], COR_NS)
if existing_param is not None:
remove_node_with_whitespace(existing_param)
print(f'[OK] Replaced outputParameter "{parsed["key"]}" in variant "{var_name}"')
dirty = True; print(f'[OK] Replaced outputParameter "{parsed["key"]}" in variant "{var_name}"')
else:
print(f'[OK] OutputParameter "{parsed["key"]}" added to variant "{var_name}"')
dirty = True; print(f'[OK] OutputParameter "{parsed["key"]}" added to variant "{var_name}"')
frag_xml = build_output_param_fragment(parsed, output_indent)
nodes = import_fragment(xml_doc, frag_xml)
@@ -2136,7 +2213,7 @@ elif operation == "set-structure":
for node in nodes:
insert_before_element(settings, node, ref_node, settings_indent)
print(f'[OK] Structure set in variant "{var_name}": {value_arg}')
dirty = True; print(f'[OK] Structure set in variant "{var_name}": {value_arg}')
elif operation == "modify-structure":
settings = resolve_variant_settings()
@@ -2210,12 +2287,12 @@ elif operation == "modify-structure":
f'{item_indent}\t<dcsset:periodAdditionEnd xsi:type="xs:dateTime">0001-01-01T00:00:00</dcsset:periodAdditionEnd>',
f'{item_indent}</dcsset:item>',
]
frag_xml = "\r\n".join(lines)
frag_xml = "\n".join(lines)
for node in import_fragment(xml_doc, frag_xml):
insert_before_element(gi_el, node, None, item_indent)
desc = "details" if not t["groupBy"] else ", ".join(t["groupBy"])
print(f'[OK] Group "{t["name"]}" groupItems updated: {desc}')
dirty = True; print(f'[OK] Group "{t["name"]}" groupItems updated: {desc}')
elif operation == "add-dataSetLink":
for val in values:
@@ -2244,7 +2321,7 @@ elif operation == "add-dataSetLink":
desc = f"{parsed['source']} > {parsed['dest']} on {parsed['sourceExpr']} = {parsed['destExpr']}"
if parsed.get("parameter"):
desc += f" [param {parsed['parameter']}]"
print(f'[OK] DataSetLink "{desc}" added')
dirty = True; print(f'[OK] DataSetLink "{desc}" added')
elif operation == "add-dataSet":
child_indent = get_child_indent(xml_doc)
@@ -2286,7 +2363,7 @@ elif operation == "add-dataSet":
for node in nodes:
insert_before_element(xml_doc, node, ref_node, child_indent)
print(f'[OK] DataSet "{parsed["name"]}" added (dataSource={ds_source_name})')
dirty = True; print(f'[OK] DataSet "{parsed["name"]}" added (dataSource={ds_source_name})')
elif operation == "add-variant":
child_indent = get_child_indent(xml_doc)
@@ -2325,7 +2402,7 @@ elif operation == "add-variant":
for node in nodes:
insert_before_element(xml_doc, node, ref_node, child_indent)
print(f'[OK] Variant "{parsed["name"]}" ["{parsed["presentation"]}"] added')
dirty = True; print(f'[OK] Variant "{parsed["name"]}" ["{parsed["presentation"]}"] added')
elif operation == "add-conditionalAppearance":
settings = resolve_variant_settings()
@@ -2348,7 +2425,7 @@ elif operation == "add-conditionalAppearance":
desc += f" when {flt['field']} {flt['op']}"
if parsed.get("fields"):
desc += f" for {', '.join(parsed['fields'])}"
print(f'[OK] ConditionalAppearance "{desc}" added to variant "{var_name}"')
dirty = True; print(f'[OK] ConditionalAppearance "{desc}" added to variant "{var_name}"')
elif operation == "clear-selection":
settings = resolve_variant_settings()
@@ -2356,7 +2433,7 @@ elif operation == "clear-selection":
selection = find_first_element(settings, ["selection"], SET_NS)
if selection is not None:
clear_container_children(selection)
print(f'[OK] Selection cleared in variant "{var_name}"')
dirty = True; print(f'[OK] Selection cleared in variant "{var_name}"')
else:
print(f'[INFO] No selection section in variant "{var_name}"')
@@ -2366,7 +2443,7 @@ elif operation == "clear-order":
order_el = find_first_element(settings, ["order"], SET_NS)
if order_el is not None:
clear_container_children(order_el)
print(f'[OK] Order cleared in variant "{var_name}"')
dirty = True; print(f'[OK] Order cleared in variant "{var_name}"')
else:
print(f'[INFO] No order section in variant "{var_name}"')
@@ -2376,7 +2453,7 @@ elif operation == "clear-filter":
filter_el = find_first_element(settings, ["filter"], SET_NS)
if filter_el is not None:
clear_container_children(filter_el)
print(f'[OK] Filter cleared in variant "{var_name}"')
dirty = True; print(f'[OK] Filter cleared in variant "{var_name}"')
else:
print(f'[INFO] No filter section in variant "{var_name}"')
@@ -2386,7 +2463,7 @@ elif operation == "clear-conditionalAppearance":
ca_el = find_first_element(settings, ["conditionalAppearance"], SET_NS)
if ca_el is not None:
clear_container_children(ca_el)
print(f'[OK] ConditionalAppearance cleared in variant "{var_name}"')
dirty = True; print(f'[OK] ConditionalAppearance cleared in variant "{var_name}"')
else:
print(f'[INFO] No conditionalAppearance section in variant "{var_name}"')
@@ -2430,7 +2507,7 @@ elif operation == "modify-filter":
uid = new_uuid() if parsed["userSettingID"] == "auto" else parsed["userSettingID"]
set_or_create_child_element(filter_item, "userSettingID", SET_NS, uid, item_indent)
print(f'[OK] Filter "{parsed["field"]}" modified in variant "{var_name}"')
dirty = True; print(f'[OK] Filter "{parsed["field"]}" modified in variant "{var_name}"')
elif operation == "modify-dataParameter":
settings = resolve_variant_settings()
@@ -2473,7 +2550,7 @@ elif operation == "modify-dataParameter":
else:
val_lines.append(f'{item_indent}<dcscor:value xsi:type="xs:string">{esc_xml(str(pv))}</dcscor:value>')
val_xml = "\r\n".join(val_lines)
val_xml = "\n".join(val_lines)
val_nodes = import_fragment(xml_doc, val_xml)
for node in val_nodes:
insert_before_element(dp_item, node, None, item_indent)
@@ -2496,7 +2573,7 @@ elif operation == "modify-dataParameter":
uid = new_uuid() if parsed["userSettingID"] == "auto" else parsed["userSettingID"]
set_or_create_child_element(dp_item, "userSettingID", SET_NS, uid, item_indent)
print(f'[OK] DataParameter "{parsed["parameter"]}" modified in variant "{var_name}"')
dirty = True; print(f'[OK] DataParameter "{parsed["parameter"]}" modified in variant "{var_name}"')
elif operation == "modify-field":
ds_node = resolve_data_set()
@@ -2519,6 +2596,13 @@ elif operation == "modify-field":
"type": parsed["type"] if parsed.get("type") else existing["type"],
"roles": parsed["roles"] if parsed.get("roles") else existing["roles"],
"restrict": parsed["restrict"] if parsed.get("restrict") else existing["restrict"],
# Preserve raw <valueType> only when user did NOT override type via shorthand.
"rawValueType": None if parsed.get("type") else existing.get("_rawValueType"),
# Preserve raw multi-lang title; pass existing ru content for change detection.
"_rawTitle": existing.get("_rawTitle"),
"_existingTitleRu": existing.get("title"),
# Pass-through unknown children (e.g. <editFormat>, <appearance>, custom extensions).
"_unknownChildren": existing.get("_unknownChildren"),
}
# Find next element sibling for position
@@ -2540,7 +2624,7 @@ elif operation == "modify-field":
for node in nodes:
insert_before_element(ds_node, node, next_sib, child_indent)
print(f'[OK] Field "{field_name}" modified in dataset "{ds_name}"')
dirty = True; print(f'[OK] Field "{field_name}" modified in dataset "{ds_name}"')
elif operation == "set-field-role":
ds_node = resolve_data_set()
@@ -2571,14 +2655,30 @@ elif operation == "set-field-role":
field_indent = get_child_indent(field_el)
# Remove existing <role>
# Remove existing <role> — but first capture OuterXml of sub-children that the
# rebuild won't re-emit (custom <dcscom:groupFields>, <dcscom:addition>, etc.).
old_role = next((ch for ch in field_el if isinstance(ch.tag, str) and local_name(ch) == "role" and etree.QName(ch.tag).namespace == SCH_NS), None)
known_role_children = {"periodNumber", "periodType", "dimension", "ignoreNullsInGroups",
"balance", "account", "accountTypeExpression", "additionType", "addition"}
kv_keys = {k for k, _ in kv}
preserved_role_children = []
if old_role is not None:
for gc in old_role:
if not isinstance(gc.tag, str):
continue
ln = local_name(gc)
if ln in known_role_children:
continue
if ln in kv_keys:
continue
raw = etree.tostring(gc, encoding="unicode", with_tail=False)
raw = re.sub(r' xmlns(?::\w+)?="[^"]*"', "", raw)
preserved_role_children.append(raw)
remove_node_with_whitespace(old_role)
# Empty spec — remove only
if not flags and not kv:
print(f'[OK] Field "{data_path}" role cleared')
dirty = True; print(f'[OK] Field "{data_path}" role cleared')
continue
# Build new <role>
@@ -2591,8 +2691,10 @@ elif operation == "set-field-role":
lines.append(f"{field_indent}\t<dcscom:{flag}>true</dcscom:{flag}>")
for k, v in kv:
lines.append(f"{field_indent}\t<dcscom:{k}>{esc_xml(v)}</dcscom:{k}>")
for raw in preserved_role_children:
lines.append(f"{field_indent}\t" + raw)
lines.append(f"{field_indent}</role>")
frag_xml = "\r\n".join(lines)
frag_xml = "\n".join(lines)
ref_node = next((ch for ch in field_el if isinstance(ch.tag, str) and local_name(ch) in ("valueType", "inputParameters") and etree.QName(ch.tag).namespace == SCH_NS), None)
for node in import_fragment(xml_doc, frag_xml):
@@ -2603,7 +2705,7 @@ elif operation == "set-field-role":
parts.append(" ".join(f"@{f}" for f in flags))
if kv:
parts.append(" ".join(f"{k}={v}" for k, v in kv))
print(f'[OK] Field "{data_path}" role set: {" ".join(parts)}')
dirty = True; print(f'[OK] Field "{data_path}" role set: {" ".join(parts)}')
elif operation == "remove-field":
ds_node = resolve_data_set()
@@ -2615,7 +2717,7 @@ elif operation == "remove-field":
print(f'[WARN] Field "{field_name}" not found in dataset "{ds_name}"')
continue
remove_node_with_whitespace(field_el)
print(f'[OK] Field "{field_name}" removed from dataset "{ds_name}"')
dirty = True; print(f'[OK] Field "{field_name}" removed from dataset "{ds_name}"')
try:
settings = resolve_variant_settings()
@@ -2625,7 +2727,7 @@ elif operation == "remove-field":
sel_item = find_element_by_child_value(selection, "item", "field", field_name, SET_NS)
if sel_item is not None:
remove_node_with_whitespace(sel_item)
print(f'[OK] Field "{field_name}" removed from selection of variant "{var_name}"')
dirty = True; print(f'[OK] Field "{field_name}" removed from selection of variant "{var_name}"')
except SystemExit:
pass
@@ -2637,7 +2739,7 @@ elif operation == "remove-total":
print(f'[WARN] TotalField "{data_path}" not found')
continue
remove_node_with_whitespace(total_el)
print(f'[OK] TotalField "{data_path}" removed')
dirty = True; print(f'[OK] TotalField "{data_path}" removed')
elif operation == "remove-calculated-field":
for val in values:
@@ -2647,7 +2749,7 @@ elif operation == "remove-calculated-field":
print(f'[WARN] CalculatedField "{data_path}" not found')
continue
remove_node_with_whitespace(calc_el)
print(f'[OK] CalculatedField "{data_path}" removed')
dirty = True; print(f'[OK] CalculatedField "{data_path}" removed')
try:
settings = resolve_variant_settings()
@@ -2657,7 +2759,7 @@ elif operation == "remove-calculated-field":
sel_item = find_element_by_child_value(selection, "item", "field", data_path, SET_NS)
if sel_item is not None:
remove_node_with_whitespace(sel_item)
print(f'[OK] Field "{data_path}" removed from selection of variant "{var_name}"')
dirty = True; print(f'[OK] Field "{data_path}" removed from selection of variant "{var_name}"')
except SystemExit:
pass
@@ -2669,7 +2771,7 @@ elif operation == "remove-parameter":
print(f'[WARN] Parameter "{param_name}" not found')
continue
remove_node_with_whitespace(param_el)
print(f'[OK] Parameter "{param_name}" removed')
dirty = True; print(f'[OK] Parameter "{param_name}" removed')
elif operation == "remove-filter":
settings = resolve_variant_settings()
@@ -2685,7 +2787,7 @@ elif operation == "remove-filter":
print(f'[WARN] Filter for "{field_name}" not found in variant "{var_name}"')
continue
remove_node_with_whitespace(filter_item)
print(f'[OK] Filter for "{field_name}" removed from variant "{var_name}"')
dirty = True; print(f'[OK] Filter for "{field_name}" removed from variant "{var_name}"')
elif operation == "add-drilldown":
# String-based manipulation — templates use dcsat namespace with inline xmlns
@@ -2816,7 +2918,7 @@ elif operation == "add-drilldown":
cell_count += 1
search_start = cell_end + 1
print(f"[OK] {drill_name} \u2192 {tpl_name} (param + {cell_count} cell(s))")
dirty = True; print(f"[OK] {drill_name} \u2192 {tpl_name} (param + {cell_count} cell(s))")
# Apply insertions in reverse order to preserve offsets.
# For same position: reverse insertion order so first resource ends up first in file.
@@ -2829,13 +2931,35 @@ elif operation == "add-drilldown":
with open(resolved_path, "wb") as f:
f.write(b'\xef\xbb\xbf')
f.write(raw_text.encode("utf-8"))
print(f"[OK] Saved {resolved_path}")
dirty = True; print(f"[OK] Saved {resolved_path}")
sys.exit(0)
# ── 9. Save ─────────────────────────────────────────────────
if not dirty:
print("[INFO] No changes -- file untouched")
sys.exit(0)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
# Format-preserve post-processing (mirrors PS path):
# (1) restore the original raw <DataCompositionSchema ...> opening tag — lxml collapses
# multi-line xmlns into one line.
xml_text = xml_bytes.decode("utf-8")
if raw_root_opening:
xml_text = re.sub(r"<DataCompositionSchema\b[^>]*>", lambda m: raw_root_opening, xml_text, count=1, flags=re.DOTALL)
# Normalize self-closing tags: lxml writes `<foo bar="x"/>` already (no space), but be
# defensive — strip any space before `/>` so PS and PY ports stay byte-equivalent.
xml_text = re.sub(r"(?<=\S) />", "/>", xml_text)
# Normalize line endings to match source.
if line_ending == "\r\n":
xml_text = re.sub(r"(?<!\r)\n", "\r\n", xml_text)
else:
xml_text = xml_text.replace("\r\n", "\n")
xml_bytes = xml_text.encode("utf-8")
if not xml_bytes.endswith(b"\n"):
xml_bytes += b"\n"
with open(resolved_path, "wb") as f:
+149 -162
View File
@@ -15,7 +15,6 @@ Tests live next to the project they cover (not inside the skill). Convention: `t
| Goal | Mode |
|------|------|
| Explore a form, prototype a single step, debug one selector | `exec` (interactive session) |
| **Walk through a scenario live before committing it as a test** | `exec` first, then `test` |
| Reproduce a bug as a failing test before fixing it | `test` |
| Cover a feature so future changes are checked automatically | `test` |
| Run the project's regression on a new build | `test` |
@@ -25,73 +24,15 @@ Don't write a `.test.mjs` for a one-shot user request. Don't drive a regression
## Before writing tests — recon
Two layers, in order. Don't skip either.
Two layers, in order.
### 1. Static recon — metadata
**1. Static recon — metadata.** Never invent identifiers. For every metadata object the user mentions, run the matching info skill first: `/meta-info` (attributes/tabular sections), `/form-info` (form layout), `/skd-info` (DCS), `/mxl-info` (templates), `/role-info` (rights), `/subsystem-info` (composition / command interface). If the user names objects you can't find — stop and ask.
Never invent identifiers. For every metadata object the user mentions (or that you decide to cover), run the matching info skill first:
**2. Live recon — interactive walkthrough.** For any non-trivial scenario, walk the path live in `exec` mode before transcribing it. Metadata tells you what exists; the live walkthrough tells you what actually happens. Capture from `getFormState()`: exact button names (`'Провести и закрыть'`, not `'Сохранить'`), table section names for multi-grid forms, required fields, places where a real async wait is needed. Then transcribe the working sequence into `*.test.mjs`, wrapping logical chunks in `step('...', async () => { ... })`.
| Object type | Skill |
|-------------|-------|
| Catalog/document/register attributes, tabular sections | `/meta-info` |
| Form layout — fields, buttons, tabs, tables | `/form-info` |
| DCS report — fields, parameters, filters | `/skd-info` |
| Spreadsheet template areas/parameters | `/mxl-info` |
| Role rights / restrictions | `/role-info` |
| Subsystem composition / command interface | `/subsystem-info` |
The mechanics of `exec` / `getFormState` / `fillFields` / `clickElement` are in [SKILL.md](SKILL.md) — read it before recon if you haven't already.
This gives the real Russian field labels, command names, column headers, table-section names. Without it, fuzzy matching will silently land on the wrong element, or fail with no useful diagnostic.
If the user names objects you cannot find: stop and ask. Do not guess.
### 2. Live recon — interactive walkthrough
For any non-trivial scenario, walk the path live in `exec` mode before writing it down. Metadata tells you what exists; the live walkthrough tells you what actually happens — which button posts the document, which dialog 1C raises, how the form looks after `clickElement('Создать')`, what fields are required, where `wait()` is genuinely needed.
```bash
# Start a session (background).
node $RUN start http://localhost:9191/myapp/ru_RU
# Step the scenario interactively. After each step, inspect.
cat <<'EOF' | node $RUN exec -
await navigateSection('Склад');
const cmds = await getCommands();
console.log(cmds);
EOF
cat <<'EOF' | node $RUN exec -
await openCommand('Приходная накладная');
await clickElement('Создать');
const s = await getFormState();
console.log(JSON.stringify(s.fields.map(f => ({ name: f.name, label: f.label, required: f.required })), null, 2));
console.log('buttons:', s.buttons.map(b => b.name));
console.log('tables:', s.tables.map(t => ({ name: t.name, label: t.label, columns: t.columns })));
EOF
# Try the actions you plan to encode. If a step fails, fix and re-try
# before transcribing it.
cat <<'EOF' | node $RUN exec -
await fillFields({ 'Контрагент': 'ООО Север' });
await fillTableRow({ 'Номенклатура': 'Товар 01', 'Количество': '5' },
{ table: 'Товары', add: true });
await clickElement('Провести и закрыть');
console.log(JSON.stringify(await getFormState()));
EOF
# When done, stop the session (or leave it for the next test you write).
node $RUN stop
```
What to record from the walkthrough into the test:
- Exact button names (`'Провести и закрыть'`, not `'Сохранить'`).
- Field labels as 1C renders them (with possible non-breaking spaces — `fillFields` normalises, but be exact).
- Table section names from `getFormState().tables[].name`/`label` for multi-grid forms.
- Required `wait()` durations — only where a real async event happens (report generation, server-side calculation). Default actions await internally.
- The shape of `getFormState()` after each action — gives you the right `assert.equal(...)` paths.
After this, transcribe the working sequence into `*.test.mjs`, wrap each chunk in `step('...', async () => { ... })`, add assertions for the invariants you saw. Run the file once with `node $RUN test path/to/file.test.mjs` to confirm.
When live recon is overkill: trivial reads (`navigateSection` + `readTable` + assert non-empty), or scenarios you've already proven once in this session. When it's essential: anything with confirmation dialogs, posting/cancellation flows, reports with custom filters, multi-grid forms, or user-customised forms you've never seen.
When live recon is overkill: trivial reads (`navigateSection` + `readTable` + assert non-empty), or scenarios you've already proven once in this session. When it's essential: confirmation dialogs, posting/cancellation flows, reports with custom filters, multi-grid forms, user-customised forms.
## Suite layout
@@ -99,30 +40,20 @@ When live recon is overkill: trivial reads (`navigateSection` + `readTable` + as
```
tests/
web-test/ # engine self-tests (reserved if our repo layout)
<app-name>/ # application regression — one per solution
_hooks.mjs
webtest.config.mjs
_allure/ # optional static Allure config
01-login/
02-counterparties/
...
<another-app>/ # second solution, fully isolated
_hooks.mjs
...
```
`<app-name>` is the project/extension slug (`acc-payroll`, `erp-customisation`, etc.). Pick something stable and pass it on the CLI:
```bash
node $RUN test tests/<app-name>/
```
Inside the application subfolder, organize by **feature**, not by metadata kind. Numeric prefixes on both folder and file enforce run order (discovery is alphabetic by full path).
Inside the application subfolder, organize by **feature**, not by metadata kind. Numeric prefixes on both folder and file enforce run order — discovery walks recursively and sorts files by full relative path; entries starting with `_` or `.` are skipped (so `_hooks.mjs`, `_allure/` won't be picked up as tests).
```
tests/<app-name>/
_hooks.mjs # stand prep + cross-cutting hooks (optional)
webtest.config.mjs # url, contexts, defaults (optional)
01-login/
01-open-base.test.mjs
02-section-navigation.test.mjs
@@ -132,15 +63,11 @@ tests/<app-name>/
03-goods-receipt/
01-fill.test.mjs
02-post.test.mjs
03-unpost.test.mjs
04-balance-report/
01-generate.test.mjs
02-warehouse-filter.test.mjs
05-approval-process/
01-end-to-end.test.mjs # multi-user
```
Per-folder `_hooks.mjs` / `webtest.config.mjs` inside the application subfolder are NOT supported. Only the application-root copies are loaded.
Per-folder `_hooks.mjs` / `webtest.config.mjs` inside the application subfolder are NOT supported — only the application-root copies are loaded.
## Test file anatomy
@@ -185,18 +112,95 @@ export default async function(ctx) {
}
```
The runner injects every `browser.mjs` export into `ctx` plus `assert`, `step`, `log`, `testInfo`, `testResult` (afterEach only). For multi-context tests, each context name is its own scoped namespace (`ctx.clerk.clickElement(...)` etc.) — `step`/`assert` stay top-level.
**Step names — in Russian, descriptive.** Step labels surface in the console output, in JSON/JUnit, and as Allure step nodes. Russian-speaking QA reads them. Use a full action phrase (`'Создать нового контрагента'`), not a tag (`'create'`) and not a transliteration. Same applies to `export const name` and `displayName` in `webtest.config.mjs`.
**Step names — in Russian, descriptive.** Step labels surface in the console output, in JSON/JUnit, and as Allure step nodes. Russian-speaking QA reads them. Use a full action phrase (`'Создать нового контрагента'`, `'Проверить наличие документа в списке'`), not a tag (`'create'`, `'verify'`) and not a transliteration. Same applies to `export const name` and `displayName` in `webtest.config.mjs`.
## `ctx` contract
The runner injects every `browser.mjs` export into `ctx` (all 1C action functions auto-detect platform errors — see SKILL.md), plus the test utilities below.
### Test utilities
```js
step(name, fn) // async wrapper. Records start/stop. Nested calls supported.
// On throw: marks the step failed, re-throws.
// On screenshot='every-step': captures after fn().
log(...args) // adds a line to ctx.testInfo's output (goes into JSON / Allure
// attachment). Use instead of console.log inside tests.
assert.* // see "Assertions" below
```
### `ctx.testInfo` (always set, read-only)
```js
{
name, // 'Навигация по разделам' (with params substituted)
file, // '01-navigation.test.mjs' (basename)
filePath, // relative path inside testDir
tags, // ['nav', 'smoke']
timeout, // ms
attempt, // 1..maxAttempts (1-based)
maxAttempts, // 1 + retry
param, // { ... } | undefined (only when export const params is set)
contexts: { // mirrors config.contexts; includes custom fields like displayName
clerk: { url, isolation, displayName, ... },
manager: { ... },
},
primaryContext, // 'clerk' — name of the context active at test entry
// (= t.context for single, t.contexts[0] for multi)
}
```
### `ctx.testResult` (only in `afterEach`)
```js
{
status, // 'passed' | 'failed'
duration, // ms
attempts, // attempts actually executed
error, // { message, step?, screenshot? } | null
steps, // array of step results (each: { name, start, stop, status, error?, steps[] })
}
```
### Context shape
- **Single-context (default or `export const context = 'manager'`):** all API on `ctx` top-level — `ctx.clickElement(...)`, `ctx.getFormState()`, etc.
- **Multi-context (`export const contexts = ['clerk', 'manager']`):** each name is its own scoped namespace — `ctx.clerk.clickElement(...)`, `ctx.manager.fillFields(...)`. `step`, `assert`, `log`, `testInfo` stay top-level. Scoped methods auto-switch the active page before each call.
## Assertions
All on `ctx.assert`. Throw `AssertionError` with `.message`, `.actual`, `.expected`. No dependencies.
```js
// generic
assert.ok(value, msg?) // truthy
assert.equal(actual, expected, msg?) // ===
assert.notEqual(actual, expected, msg?) // !==
assert.deepEqual(actual, expected, msg?) // JSON-compare
assert.includes(haystack, needle, msg?) // string.includes / array.includes
assert.match(string, regex, msg?) // regex.test(string)
await assert.throws(asyncFn, msg?) // passes if fn throws (use await)
// 1C-specific — operate on getFormState() / readTable() output
assert.formHasField(state, 'Контрагент', msg?) // state.fields[name] exists
assert.formTitle(state, expected, msg?) // state.title includes expected
assert.tableHasRow(table, predicate, msg?) // predicate: object (partial match) or fn(row) => bool
// object form: { 'Наименование': 'Тест' }
// fn form: r => r['Сумма'] > 100
assert.tableRowCount(table, expected, msg?) // table.rows.length === expected
assert.noErrors(state, msg?) // !state.errors
```
Beyond these, just use plain JS (`throw new Error(...)`) — there's no custom matcher extension API. The 1C-specific helpers are the ones worth preferring over hand-rolled equivalents because their error messages name the actual fields/rows present, which speeds up triage.
## webtest.config.mjs
```js
export default {
// Single-context: just url.
// Single-context shorthand:
url: 'http://localhost:9191/myapp/ru_RU',
// OR multi-context: named contexts. Each test picks via `context`/`contexts` exports.
// OR multi-context:
// contexts: {
// clerk: { url: 'http://localhost:9191/myapp-clerk/ru_RU', displayName: 'Кладовщик' },
// manager: { url: 'http://localhost:9191/myapp-manager/ru_RU', displayName: 'Менеджер' },
@@ -205,7 +209,7 @@ export default {
timeout: 30000,
retries: 0,
screenshot: 'on-failure',
screenshot: 'on-failure', // 'every-step' | 'off'
record: false,
// Severity → tags mapping for Allure. Each tag at most one bucket.
@@ -217,7 +221,7 @@ export default {
};
```
CLI flags override config. Recommend latin context IDs + Russian `displayName` for video badges.
CLI flags override config. Use latin context IDs + Russian `displayName` for ergonomics — `ctx.testInfo.contexts.clerk.displayName` is friendlier than mixed-case Cyrillic keys.
## _hooks.mjs
@@ -228,74 +232,44 @@ import { execSync } from 'child_process';
// Infra — runs once around the whole suite.
export async function prepare({ hookArgs, log, config }) {
// Restore DB, publish to Apache, build EPF, etc.
// hookArgs = everything after `--` on the CLI. Parse yourself.
if (hookArgs.includes('--rebuild-stand')) { /* full rebuild */ }
// Use idempotent hash-locks to skip work on warm starts.
// hookArgs: everything after `--` on the CLI, as a string[]. Parse yourself.
const force = hookArgs.includes('--rebuild-stand');
const dataArg = hookArgs.find(a => a.startsWith('--data='))?.slice('--data='.length);
log('preparing stand, force=', force, 'data=', dataArg);
// Idempotent hash-locks on inputs (config sources, EPF spec, DB dump) keep
// warm starts to a liveness probe.
}
export async function cleanup({ log, config }) {
// Tear down or leave the stand running. Choose per project.
}
export async function cleanup({ log, config }) { /* optional */ }
// Testlevel — runs with browser ctx.
export async function beforeAll(ctx) { /* once after first context opens */ }
export async function afterAll(ctx) { /* once before final teardown */ }
export async function beforeEach(ctx) { /* ctx.testInfo is set */ }
export async function afterEach(ctx) { /* ctx.testResult is set */ }
export async function afterEach(ctx) { /* ctx.testInfo + ctx.testResult set */ }
// Per-context — runs whenever a context is created/closed.
export async function afterOpenContext(ctx, name, spec) { /* spec = config.contexts[name] */ }
export async function beforeCloseContext(ctx, name, spec) { }
```
Built-in state reset (`dismissPendingErrors` + close all forms) runs after `afterEach` automatically. Don't reimplement it.
Built-in state reset (`dismissPendingErrors` + close all forms) runs after `afterEach` automatically. Don't reimplement it in `afterEach`.
Pass hook args after `--`:
```bash
node $RUN test tests/<app-name>/ --bail -- --rebuild-stand --data=demo
└─runner─┘ └────── hookArgs ─────────┘
```
**Where to put data setup:**
- DB restore, publication, EPF build → `prepare()`. Make it idempotent (hash-locks on inputs — config sources, EPF spec, DB dump) so warm starts skip everything but a liveness probe.
- Test-specific seed data (the document this test will edit, the counterparty it expects) → per-test `setup`.
- DB restore, publication, EPF build → `prepare()`. Make it idempotent (hash-locks).
- Test-specific seed data → per-test `setup`.
- Shared session-wide warmup → `beforeAll`.
## Ready-to-paste patterns
### Catalog full cycle
```js
await step('Создать контрагента', async () => {
await navigateSection('Продажи');
await openCommand('Контрагенты');
await clickElement('Создать');
await fillFields({ 'Наименование': 'ТД Тест', 'ИНН': '7707083893' });
await clickElement('Записать и закрыть');
});
await step('Проверить наличие в списке', async () => {
const t = await readTable({ maxRows: 50 });
assert.tableHasRow(t, { 'Наименование': 'ТД Тест' });
});
await step('Удалить контрагента и подтвердить удаление', async () => {
await clickElement('ТД Тест');
const page = await getPage();
await page.keyboard.press('Delete');
await clickElement('Да');
});
```
### Document create + post
```js
const marker = 'Тест-' + Date.now();
await openCommand('Приходная накладная');
await clickElement('Создать');
await fillFields({ 'Контрагент': 'ООО Север', 'Комментарий': marker });
await fillTableRow(
{ 'Номенклатура': 'Товар 01', 'Количество': '5', 'Цена': '100' },
{ table: 'Товары', add: true }
);
await clickElement('Провести и закрыть');
// Verify: re-open list, filter or scan, assert by `marker`.
```
Use a unique marker (`Date.now()` or random suffix) so re-runs don't collide. Identify your own row by it, not by position or natural keys that may already exist in the DB.
A minimal CRUD shape is in *Test file anatomy* above — use it as the rhythm for catalog/document tests, swapping in the right section/command/fields. The patterns below cover what's specific to the regression engine, not the browser API (those live in SKILL.md).
### DCS report
@@ -335,7 +309,7 @@ export default async function({ clerk, manager, step, assert }) {
});
await step('Кладовщик видит новый статус', async () => {
const s = await clerk.getFormState();
assert.equal(s.fields.find(f => f.name === 'Статус')?.value, 'Утверждён');
assert.equal(s.fields['Статус']?.value, 'Утверждён');
});
await step('Освободить сессию кладовщика', async () => {
await manager.closeContext('clerk'); // free a 1C license for the next test
@@ -343,7 +317,7 @@ export default async function({ clerk, manager, step, assert }) {
}
```
License caveat: stock 1C allows ~2 web sessions concurrently. Close contexts you no longer need before the next multi-user test starts.
Close contexts you no longer need (`manager.closeContext('clerk')`) before the next multi-user test starts — frees a 1C web-client license and stops the previous role from holding state.
### Failing-test repro
@@ -356,37 +330,50 @@ export default async function({ openCommand, clickElement, getFormState, assert,
await clickElement('Создать');
await clickElement('Провести');
const s = await getFormState();
assert.ok(s.errorModal || s.fields.find(f => f.name === 'Контрагент')?.required,
assert.ok(s.errorModal || s.fields['Контрагент']?.required,
'Должна быть ошибка валидации или поле помечено обязательным');
}
```
Write it red first, hand it to the user, fix the underlying issue, re-run green.
### Parameterised test
```js
export const name = 'Заполнение поля {type}';
export const params = [
{ type: 'String', field: 'Наименование', value: 'Тест' },
{ type: 'Number', field: 'Цена', value: '100.50' },
{ type: 'Date', field: 'ДатаПоступления', value: '01.01.2024' },
];
export default async function({ fillFields, getFormState, assert }, { type, field, value }) {
await fillFields({ [field]: value });
const state = await getFormState();
assert.equal(state.fields[field]?.value, String(value));
}
```
Each `params` entry becomes its own test in the report. `{key}` placeholders in `name` get substituted; without placeholders, a `[index]` suffix is added. `ctx.testInfo.param` carries the current row.
## Running
```bash
node $RUN test tests/<app-name>/ # full app suite
node $RUN test tests/<app-name>/03-goods-receipt/ # one feature folder
node $RUN test tests/<app-name>/ # full app suite
node $RUN test tests/<app-name>/03-goods-receipt/ # one feature folder
node $RUN test tests/<app-name>/02-counterparties/01-create.test.mjs # one file
node $RUN test tests/<app-name>/ --tags=smoke # by tag (intersection)
node $RUN test tests/<app-name>/ --grep='накладн' # by name regex
node $RUN test tests/<app-name>/ --bail --retry=1 # stop on first fail, allow 1 retry
node $RUN test tests/<app-name>/ --tags=smoke # by tag (intersection)
node $RUN test tests/<app-name>/ --grep='накладн' # by name regex
node $RUN test tests/<app-name>/ --bail --retry=1 # stop on first fail, allow 1 retry
node $RUN test tests/<app-name>/ --report=allure-results --format=allure --report-dir=allure-results
node $RUN test tests/<app-name>/ -- --rebuild-stand # everything after `--` goes to hooks
node $RUN test tests/<app-name>/ -- --rebuild-stand # after `--` hookArgs
```
Default report is JSON when `--report=…` is given. Allure needs `--format=allure` + a directory. JUnit similarly with `--format=junit`.
### Allure static config — `_allure/` directory
### Allure static config — `_allure/`
The runner copies `<testDir>/_allure/` into the report directory before generating Allure output. Standard Allure convention applies — three files are typically used:
- **`categories.json`** — failure classification. Always emit this when setting up a suite, with 1C-specific patterns: license pool exhaustion (`Не обнаружено свободной лицензии`), 1C application errors (`ВызватьИсключение|Произошла ошибка|…`), navigation/element lookup misses, runner timeouts, assertion failures.
- **`environment.properties`** — `key=value` lines for the Environment widget. Useful when the suite runs across builds/branches (URL, 1C platform version, git branch, configuration version). Often emitted dynamically by `prepare()` rather than committed as a static file.
- **`executor.json`** — CI metadata (Jenkins URL, GitHub run ID, etc.). Only relevant when the suite runs on a CI server; for local runs, skip it.
Discovery skips the underscored directory, so it never collides with tests.
The runner copies `<testDir>/_allure/` into the report directory before generating Allure output. Drop in `categories.json` (regex-based failure classification — useful for 1C-specific buckets: license pool exhaustion, platform exceptions, runner timeouts, assertion failures), `environment.properties` (optional, often emitted dynamically by `prepare()`), `executor.json` (CI metadata, skip locally). The underscore prefix keeps the directory out of test discovery.
## Severity guidance
@@ -404,26 +391,26 @@ Don't promote everything to `critical` — it loses signal in the Allure dashboa
## Anti-patterns
- **Sleeps as a substitute for assertions.** `wait(5)` after `openCommand` is fine; `wait(30)` because something flakes is a bug — find what state you can wait on with `getFormState` instead.
- **Sleeps as a substitute for assertions.** `wait(5)` after `openCommand` is fine; `wait(30)` because something flakes is a bug — wait on `getFormState` instead.
- **Retry as a substitute for understanding.** "Not found" twice means the data isn't there or the label is wrong. Don't loop.
- **Raw DOM via `getPage().$$(...)`.** Use `getFormState`, `readTable`, `readSpreadsheet`. Raw selectors break across 1C platform versions.
- **`clickElement('×')` or `clickElement('Закрыть')`** to dismiss a form. Use `closeForm({ save: true|false })` — handles confirmation correctly.
- **Position-based row identification** (`rows[0]`) when the DB has shared seed data. Filter by unique marker or label instead.
- **Skipping recon** because "I know what this catalog looks like." You don't — the project's customisation almost certainly differs from a stock config.
- **Position-based row identification** (`rows[0]`) when the DB has shared seed data. Filter by a unique marker (`Date.now()` suffix) instead.
- **Hand-writing reset code in `afterEach`.** The runner already closes forms and dismisses errors after the hook.
- **Cross-test state assumptions.** Each test must start from the desktop and seed its own data. Order-of-execution coupling is a regression-suite trap.
- **`tags: ['smoke']` on a 90-second test.** Smoke means fast.
- **Hand-writing reset code** in `afterEach`. The runner already closes forms and dismisses errors.
- **Cross-test state assumptions.** Each test must start from desktop and seed its own data. Order-of-execution coupling is a regression-suite trap.
- **Skipping recon** because "I know what this catalog looks like." The project's customisation almost certainly differs from stock.
(General browser-API anti-patterns — raw DOM, `clickElement('Закрыть')` instead of `closeForm()` — live in SKILL.md.)
## After a run — failure triage
1. Scan the JSON or Allure summary for `failed`.
2. For each failure, read `error.message` + `error.step` + screenshot (saved next to the report).
2. For each failure, read `error.message` + `error.step` + screenshot.
3. If `error.onecError.stack` is present — it's a 1C exception, look at the platform trace.
4. Classify:
- **Test bug** — selector wrong, expectation wrong, race with no anchor → fix the test.
- **Application bug** — actual misbehaviour reproduced → report to the user with the failing step name and the platform stack.
- **Stand flake** — Apache timeout, login form not loading, license shortage → fix the hook idempotency or session-cleanup logic, not the test.
5. After fixes, re-run only the affected files (`node $RUN test tests/03-goods-receipt/`) before the full suite.
5. After fixes, re-run only the affected files before the full suite.
Report back to the user with the classification, not raw failure dumps.
+19 -6
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env node
// web-test run v1.12 — CLI runner for 1C web client automation
// web-test run v1.13 — CLI runner for 1C web client automation
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
/**
* CLI runner for 1C web client automation.
@@ -728,8 +728,11 @@ async function cmdTest(rawArgs) {
// per-test teardown (always)
if (t.teardown) try { await t.teardown(ctx); } catch {}
// Expose preliminary testResult to afterEach (final testResult assembled below).
const errInfo = { message: e.message, step: e.onecError?.step, screenshot: shotFile };
// Build the error record once: shared between ctx.testResult (afterEach), lastError
// (retry-loop carry-over and console output), and the final report record.
// onecError carries the structured 1C exception payload (stack, formState, args, ...)
// produced by ACTION_FN wrappers — preserve it in the report.
const errInfo = { message: e.message, step: e.onecError?.step, screenshot: shotFile, onecError: e.onecError };
ctx.testResult = { status: 'failed', duration: elapsed(t0), attempts: attempt, error: errInfo, steps };
// afterEach (always)
if (hooks.afterEach) try { await hooks.afterEach(ctx); } catch {}
@@ -742,9 +745,9 @@ async function cmdTest(rawArgs) {
if (videoFile) {
try { await browser.stopRecording(); } catch {}
}
lastError = { message: e.message, step: e.onecError?.step, screenshot: shotFile };
lastError = errInfo;
const dur = elapsed(t0);
testResult = { name: t.name, file: t.file, tags: t.tags, contexts: testContextNames, severity: t.severity, status: 'failed', duration: dur, attempts: attempt, start: t0, stop: Date.now(), steps, output: output.join('\n'), error: lastError, screenshot: shotFile, video: videoFile };
testResult = { name: t.name, file: t.file, tags: t.tags, contexts: testContextNames, severity: t.severity, status: 'failed', duration: dur, attempts: attempt, start: t0, stop: Date.now(), steps, output: output.join('\n'), error: errInfo, screenshot: shotFile, video: videoFile };
}
}
@@ -882,7 +885,17 @@ function writeAllure(results, reportDir, severityIndex) {
],
};
if (tr.status === 'failed' && tr.error) {
out.statusDetails = { message: tr.error.message || '', trace: tr.output || '' };
// Allure UI shows statusDetails.trace right next to the error message. We compose it
// from the test's log() output plus, when present, the platform 1C stack — so the
// raw call chain is visible without opening attachments.
const traceParts = [];
if (tr.output) traceParts.push(tr.output);
const onecStack = tr.error.onecError?.stack?.raw;
if (onecStack) {
if (traceParts.length) traceParts.push('\n--- 1C stack ---\n');
traceParts.push(onecStack);
}
out.statusDetails = { message: tr.error.message || '', trace: traceParts.join('') };
}
writeFileSync(resolve(reportDir, `${uuid}-result.json`), JSON.stringify(out, null, 2));
}
+978
View File
@@ -0,0 +1,978 @@
# Регрессионное тестирование — спецификация
Техническое описание движка регрессионных тестов: инструмент исполняет описанные кодом пользовательские сценарии в веб-клиенте прикладного решения на платформе 1С и сверяет результат с ожиданиями.
Смежные документы:
- [web-test-regression-guide.md](web-test-regression-guide.md) — пользовательский гайд с быстрым стартом.
- [web-test-guide.md](web-test-guide.md) — справочник по browser-API (`clickElement`, `getFormState`, `readTable`, …), который используется внутри тестов.
- [web-test-recording-guide.md](web-test-recording-guide.md) — видеозапись, озвучка, overlays.
---
## 1. Командная строка
```
node run.mjs test [url] <dir|file> [флаги]
```
| Флаг | По умолчанию | Описание |
|------|-------------|----------|
| `--tags=smoke,crud` | (все) | Фильтр тестов по тегам (пересечение) |
| `--grep=pattern` | (все) | Фильтр тестов по имени (регулярное выражение) |
| `--bail` | false | Остановиться при первом падении |
| `--retry=N` | 0 | Повторить упавшие тесты N раз |
| `--timeout=ms` | 30000 | Таймаут на тест (мс) |
| `--report=path` | (нет) | Записать отчёт в файл (JSON или XML для `--format=junit`) |
| `--format=fmt` | json | Формат отчёта: `json` / `allure` / `junit` |
| `--report-dir=path` | dirname(report) / testDir | Каталог для скриншотов, видео, Allure-результатов |
| `--screenshot=strategy` | on-failure | `on-failure` / `every-step` / `off` |
| `--record` | false | Записывать видео для каждого теста (mp4 в `--report-dir`) |
| `-- <hookArgs…>` | — | Всё после `--` пробрасывается в `_hooks.mjs` как `hookArgs` (см. §6.1) |
URL необязателен, если в каталоге тестов есть `webtest.config.mjs`. CLI URL переопределяет URL дефолтного контекста.
### Валидация CLI
- `--screenshot=<v>` принимается только `on-failure | every-step | off`; при невалидном значении движок выводит ошибку и завершается с ненулевым кодом до старта прогона.
- `--format=<v>` принимается только `json | allure | junit`; иначе — завершение с ошибкой.
- `--format=junit` требует `--report=<path>` (иначе некуда писать XML); иначе — завершение с ошибкой.
### Режим выполнения
1. Загружается `webtest.config.mjs` (если есть).
2. Обнаруживаются файлы `*.test.mjs`, читается каждый, извлекаются метаданные.
3. Применяются фильтры `--tags` / `--grep` / `only`. Параметризованные тесты разворачиваются.
4. Запускается браузер и default-контекст (`chromium.launch()` либо `launchPersistentContext` в зависимости от `isolation`).
5. Тесты выполняются последовательно **в алфавитном порядке относительного пути файла** (внутри файла — в порядке экспорта).
6. Для каждого теста: лениво создаются нужные `BrowserContext`-ы (`ensureContext`), переключается активный, прогоняются хуки и тело, выполняется встроенный сброс состояния.
7. По завершении: финальная очистка контекстов с `beforeCloseContext`-хуками, закрытие браузера, `cleanup()`.
---
## 2. Формат тест-модуля
Каждый файл `*.test.mjs` — ES-модуль.
### Экспорты
| Экспорт | Тип | Обязателен | По умолчанию | Описание |
|---------|-----|-----------|-------------|----------|
| `name` | `string` | да | — | Читаемое имя теста |
| `default` | `async function(ctx, param?)` | да | — | Тело теста |
| `tags` | `string[]` | нет | `[]` | Теги для фильтрации |
| `timeout` | `number` | нет | 30000 | Таймаут теста (мс) |
| `skip` | `boolean \| string` | нет | false | Пропустить тест (строка = причина) |
| `only` | `boolean` | нет | false | Запустить только этот тест (отладка) |
| `context` | `string` | нет | defaultContext | Имя контекста из файла конфигурации |
| `contexts` | `string[]` | нет | — | Мульти-пользовательский процессный тест |
| `severity` | `string` | нет | — | `blocker` / `critical` / `normal` / `minor` / `trivial` |
| `params` | `object[]` | нет | — | Параметризация (см. §13) |
| `setup` | `async function(ctx)` | нет | — | Подготовка перед тестом |
| `teardown` | `async function(ctx)` | нет | — | Очистка после теста (выполняется всегда) |
### Пример: тест с одним контекстом
```js
export const name = 'CRUD справочника Контрагенты';
export const tags = ['smoke', 'crud', 'catalog'];
export const timeout = 45000;
export default async function({ navigateSection, openCommand, clickElement,
fillFields, readTable, closeForm, getFormState, assert, step, log }) {
await step('Открыть список', async () => {
await navigateSection('Склад');
await openCommand('Контрагенты');
});
await step('Создать элемент', async () => {
await clickElement('Создать');
await fillFields({ 'Наименование': 'Тест-' + Date.now() });
await clickElement('Записать и закрыть');
});
await step('Проверить в списке', async () => {
const table = await readTable();
assert.tableHasRow(table, r => r['Наименование']?.startsWith('Тест-'));
log('Элемент найден в списке');
});
}
```
### Пример: мульти-контекстный процессный тест
Рекомендация: латинский ID контекста + кириллический `displayName` в `webtest.config.mjs.contexts.<id>.displayName` (см. §7).
```js
export const name = 'Согласование приходной накладной';
export const contexts = ['clerk', 'manager'];
export const tags = ['process'];
export default async function({ clerk, manager, step }) {
await step('Кладовщик создаёт накладную', async () => {
await clerk.navigateSection('Склад');
await clerk.openCommand('Приходные накладные');
await clerk.clickElement('Создать');
await clerk.fillFields({ 'Контрагент': 'ООО Поставщик' });
await clerk.clickElement('Записать');
});
await step('Менеджер утверждает', async () => {
await manager.navigateSection('Согласование');
await manager.openCommand('На утверждении');
await manager.clickElement('ООО Поставщик', { dblclick: true });
await manager.clickElement('Утвердить');
});
await step('Освобождаем контекст clerk', async () => {
await manager.closeContext('clerk'); // освободить лицензию 1С
});
}
```
---
## 3. Объект контекста
Каждая тестовая функция получает объект контекста `ctx`.
### API браузера (все экспорты browser.mjs)
Все функции обёрнуты авто-обнаружением 1С-ошибок (как в `executeScript`):
- При модальной/всплывающей ошибке 1С: скриншот → `fetchErrorStack` → исключение с заполненным `err.onecError`.
- Обёрнутые ACTION_FNS: `clickElement`, `fillFields`, `fillField`, `selectValue`, `fillTableRow`, `deleteTableRow`, `openCommand`, `navigateSection`, `navigateLink`, `openFile`, `closeForm`, `filterList`, `unfilterList`.
Полный список доступных функций (по группам, детальное описание — в [web-test-guide.md](web-test-guide.md)):
**Навигация:** `navigateSection`, `openCommand`, `switchTab`, `navigateLink`, `openFile`
**Состояние:** `getFormState`, `getPageState`, `getSections`, `getCommands`
**Таблицы:** `readTable`, `readSpreadsheet`, `fillTableRow`, `deleteTableRow`
**Поля:** `fillFields`, `fillField`, `selectValue`
**Действия:** `clickElement`, `closeForm`, `filterList`, `unfilterList`
**Ошибки:** `fetchErrorStack`
**Контексты:** `createContext`, `setActiveContext`, `closeContext`, `listContexts`, `hasContext`, `getActiveContext`
**Запись:** `startRecording`, `stopRecording`, `isRecording`, `addNarration`, `getCaptions`
**Презентация:** `showCaption`, `hideCaption`, `showTitleSlide`, `hideTitleSlide`, `showImage`, `hideImage`, `highlight`, `unhighlight`, `setHighlight`, `isHighlightMode`
**Утилиты:** `screenshot`, `wait`, `getPage`, `getSession`, `readFileSync`, `writeFileSync`
> `dismissPendingErrors` — внутренняя функция (browser.mjs), на `ctx` не публикуется. Тест её не вызывает напрямую: она срабатывает автоматически перед каждым ACTION_FN и внутри встроенного сброса.
### Тестовые утилиты
- `step(name, fn)` — обёртка шага (см. §4)
- `assert.*` — хелперы утверждений (см. §5)
- `log(...args)` — добавить строку в вывод теста. Строки накапливаются в массив, склеиваются и попадают в JSON `tests[].output`. В Allure-отчёте `output` пишется в `statusDetails.trace` **только для упавших тестов**; для успешных теряется (отдельного вложения не создаётся).
### Метаданные теста (`ctx.testInfo`)
Декларативная информация о текущем тесте. Движок выставляет `ctx.testInfo` перед каждой попыткой (до `beforeEach`); хук и тело теста могут читать. Изменять не следует — объект используется самим движком при сборке отчёта.
```js
ctx.testInfo = {
name, // 'Навигация по разделам' (с подставленными params)
file, // '01-navigation.test.mjs' (basename)
filePath, // '01-navigation.test.mjs' (relative к testDir, разделитель '/')
tags, // ['nav', 'smoke']
timeout, // 60000 (ms)
attempt, // 1..maxAttempts (1-based)
maxAttempts, // 1 + retry
param, // { ... } | undefined (для export const params)
contexts: { // объект, всегда 1+ ключей; зеркалит config.contexts
a: { url, isolation, ...customFields },
b: { ... },
},
primaryContext, // 'a' — имя контекста, активного на входе в тест
// (= t.context для single, t.contexts[0] для multi)
}
```
Доступ к специфике контекста: `testInfo.contexts[testInfo.primaryContext].displayName`. `primaryContext` — декларация теста; не зависит от текущего значения `getActiveContext()` (которое может меняться внутри теста).
### Результат теста в afterEach (`ctx.testResult`)
Только в `afterEach`. До запуска теста — `null`. После — заполняется движком перед вызовом хука:
```js
ctx.testResult = {
status, // 'passed' | 'failed'
duration, // ms
attempts, // фактически выполнено попыток (1..maxAttempts)
error, // { message, step?, screenshot?, onecError? } | null
steps, // массив step-результатов (структура — см. §4)
}
```
В итоговый JSON-отчёт (`tests[]`) добавляются ещё `name`, `file`, `tags`, `contexts`, `severity`, `start`, `stop`, `output`, `screenshot`, `video` (см. §9). В `afterEach` они недоступны — движок собирает финальную запись после хука.
### Мульти-контекст
При `export const contexts = ['a', 'b']`:
- `ctx.a` и `ctx.b` — отдельные scoped-объекты, каждый с полным API браузера. Перед каждым вызовом scoped-обёртка переключает активный контекст через `setActiveContext`.
- `ctx.step`, `ctx.assert`, `ctx.log`, `ctx.testInfo`, `ctx.testResult` остаются на верхнем уровне.
При single-context (`export const context = 'X'` или дефолт) API публикуется плоско на `ctx`.
---
## 4. step(name, fn) — обёртка шага
```js
await step('Имя шага', async () => {
// тело шага
});
```
Поведение:
- Записывает метку `start` перед `fn()`.
- Записывает метку `stop` после `fn()` (успех или ошибка).
- При ошибке: устанавливает `status: 'failed'`, прикрепляет сообщение, пробрасывает исключение.
- При успехе: устанавливает `status: 'passed'`.
- Если стратегия скриншотов `every-step` — делает скриншот после `fn()`.
- Вложенные шаги поддерживаются (шаг внутри шага).
- Напрямую маппится на шаги Allure.
Структура данных шага (для отчётов):
```js
{
name: 'Имя шага',
start: 1712345678000, // мс от эпохи
stop: 1712345679200,
status: 'passed' | 'failed',
error: 'сообщение' | undefined,
screenshot: 'путь' | undefined,
steps: [] // вложенные шаги
}
```
---
## 5. Утверждения (assertions)
Простые хелперы без зависимостей. Бросают `AssertionError` со свойствами `.message`, `.actual`, `.expected`.
### Общие
```js
assert.ok(value, msg?) // истинность
assert.equal(actual, expected, msg?) // ===
assert.notEqual(actual, expected, msg?) // !==
assert.deepEqual(actual, expected, msg?) // сравнение через JSON
assert.includes(haystack, needle, msg?) // string/array .includes()
assert.match(string, regex, msg?) // regex.test(string)
await assert.throws(asyncFn, msg?) // ожидает исключение из async fn
```
### Специфичные для 1С
```js
assert.formHasField(state, fieldName, msg?)
// проверяет наличие state.fields[fieldName]; в сообщении об ошибке
// перечисляются доступные поля для быстрой диагностики
assert.formTitle(state, expected, msg?)
// проверяет, что state.title содержит expected
assert.tableHasRow(table, predicate, msg?)
// predicate: объект (частичное совпадение по ===) или функция row => bool
// объект: assert.tableHasRow(table, { 'Наименование': 'Тест' })
// функция: assert.tableHasRow(table, r => r['Сумма'] > 100)
assert.tableRowCount(table, expected, msg?)
// проверяет table.rows.length === expected
assert.noErrors(state, msg?)
// проверяет !state.errors
```
Расширения assert API нет. Для нестандартных проверок — `throw new Error(...)` или комбинация существующих хелперов.
---
## 6. Хуки
Все хуки определяются в `_hooks.mjs` в корне каталога тестов.
### Три уровня
**Инфраструктурный уровень** (без браузера):
- `prepare({ hookArgs, log, config })` — до подключения (восстановление БД, публикация, загрузка данных).
- `cleanup({ hookArgs, log, config })` — после отключения (удаление публикации, очистка).
Поля параметра:
- `hookArgs: string[]` — всё, что в командной строке передано после разделителя `--`, без интерпретации со стороны движка. Хук парсит сам (см. §6.1).
- `log: (...args) => void` — функция логирования движка (структурированный вывод с префиксом `[hooks]`). Использовать вместо `console.log`, чтобы не ломать формат отчёта.
- `config: object` — разобранный `webtest.config.mjs` (URL контекстов, режим изоляции, правила severity и т.д.).
**Тестовый уровень** (с контекстом браузера):
- `beforeAll(ctx)` — после подключения, перед первым тестом.
- `afterAll(ctx)` — после последнего теста, до отключения.
- `beforeEach(ctx)` — перед каждым тестом. На входе уже доступен `ctx.testInfo` (см. §3).
- `afterEach(ctx)` — после каждого теста. Дополнительно доступен `ctx.testResult` с результатом завершившегося теста.
**Контекстный уровень** (на каждый browser-контекст, жизненный цикл = создан → удалён):
- `afterOpenContext(ctx, name, spec)` — сразу после успешного `createContext`. `spec` — запись из `config.contexts[name]` со всеми пользовательскими полями (`displayName`, `url`, `isolation`, …). Полезно: вставка постоянного DOM-оверлея/бейджа, предварительная навигация в контексте, регистрация телеметрии.
- `beforeCloseContext(ctx, name, spec)` — перед `closeContext` (контекст ещё активен и работает). Полезно: сохранение остатков буферов, сбор метрик, последний скриншот. Срабатывает и при явном `ctx.closeContext(name)` из теста, и в финальной очистке движка перед `disconnect`.
`closeContext(name)` валиден только когда `name !== getActiveContext()` — иначе бросается исключение. В scoped API (`ctx.a.closeContext('b')`) это естественно: scoped-обёртка сначала вызывает `setActiveContext('a')`, потом закрывает `'b'` — целевой контекст всегда неактивен.
### Подавление ошибок в хуках
Ошибки в `afterEach`, `teardown`, `afterAll` и `cleanup` ловятся и логируются движком, но не прерывают прогон и не помечают тест/прогон как failed. Логика: пост-хуки очистки должны быть устойчивы к собственным сбоям, чтобы один сломанный `teardown` не приводил к падению остальных тестов по цепочке. Если в этих хуках произошла фатальная для регресса проблема — бросайте отдельный `Error` в `beforeAll`/`beforeEach`, чтобы он прервал прогон, либо проверяйте состояние в самом тесте.
### Порядок выполнения
```
prepare() // без браузера (восстановление БД, публикация)
browser.launch() // запуск процесса браузера
createContext(default) // первый контекст создан
afterOpenContext(ctx, default) // hook: контекст готов
beforeAll(ctx) // браузер готов, default-контекст создан
[lazy ensureContext(name)] // для multi-context тестов
afterOpenContext(ctx, name)
beforeEach(ctx)
test.setup(ctx) // подготовка теста
test.default(ctx) // тело теста (может вызвать ctx.closeContext)
[при ctx.closeContext(x)]: beforeCloseContext(ctx, x) → close(x)
test.teardown(ctx) // очистка теста (всегда)
afterEach(ctx) // всегда
[встроенный сброс] // всегда (для каждого живого контекста теста)
…следующий тест…
afterAll(ctx)
[для каждого оставшегося контекста]: beforeCloseContext(ctx, name, spec)
browser.close() // финальный disconnect (без явных closeContext —
// контексты умирают вместе с браузером)
cleanup() // без браузера (удаление публикации)
```
### Встроенный сброс состояния
После каждого теста (после `afterEach`) движок гарантирует чистое состояние:
```js
async function resetState(ctx) {
try { await ctx.dismissPendingErrors(); } catch {} // no-op на ctx (не экспортируется);
// внутренний dismiss всё равно отработает
// через ACTION_FN-обёртки ниже
for (let i = 0; i < 10; i++) {
const state = await ctx.getFormState();
if (state.form == null) break; // важно: == null, не !state.form —
// form может быть 0 (валидный idx фоновой формы)
try { await ctx.closeForm({ save: false }); } catch { break; }
}
}
```
Гарантирует, что каждый тест стартует с чистого рабочего стола, независимо от того, как завершился предыдущий (падение, таймаут, ошибка утверждения). Реимплементировать это в пользовательском `afterEach` не нужно.
### Пример _hooks.mjs
```js
import { execSync } from 'child_process';
export async function prepare({ hookArgs, log, config }) {
const force = hookArgs.includes('--rebuild-stand');
const dataArg = hookArgs.find(a => a.startsWith('--data='))?.slice('--data='.length);
log('preparing stand, force=', force, 'data=', dataArg);
execSync('powershell.exe -File scripts/restore-db.ps1');
execSync('powershell.exe -File scripts/publish.ps1');
}
export async function cleanup({ log }) {
log('cleaning up stand');
execSync('powershell.exe -File scripts/unpublish.ps1');
}
export async function beforeAll(ctx) {
// По умолчанию 1С после входа уже показывает дефолтную секцию — навигация
// в beforeAll обычно не нужна. Хук удобен для общего setup'а который
// должен случиться один раз для всего прогона.
}
export async function afterEach(ctx) {
// Доступен ctx.testResult — { status, duration, attempts, error, steps }.
// Встроенный сброс состояния выполняется ПОСЛЕ afterEach автоматически.
}
export async function afterOpenContext(ctx, name, spec) {
// Удобно для persistent DOM-overlay'я с displayName (видно в видео,
// какая вкладка к какому пользователю относится).
}
export async function beforeCloseContext(ctx, name, spec) {
// Срабатывает и при ctx.closeContext из теста, и в финальной очистке.
}
```
### 6.1. Проброс пользовательских флагов через `--`
Движок не знает о пользовательских флагах хуков. Чтобы хуки получили разовые параметры без правки `webtest.config.mjs` или окружения, используется стандартная shell-конвенция `--` (как у `npm`, `cargo`, `pytest`): всё, что идёт после `--` в CLI движка, передаётся в `prepare` / `cleanup` через поле `hookArgs: string[]` без интерпретации.
```
node run.mjs test tests/myapp/ --bail -- --rebuild-stand --reload-data
└─ runner ─┘ └────── hookArgs ────────┘
```
В этом примере движок получает `--bail`, а `hookArgs` хуков становится `['--rebuild-stand', '--reload-data']`. Парсинг этого массива — ответственность хуков.
Если разделитель `--` не указан, `hookArgs` — пустой массив. Это позволяет движку и хукам развиваться независимо: новый встроенный флаг движка никогда не пересечётся с пользовательским.
---
## 7. Файл конфигурации
`webtest.config.mjs` в корне каталога тестов. Необязателен — если отсутствует, URL должен быть передан через CLI.
```js
export default {
// Контексты: именованные URL для разных пользователей/ролей.
// Рекомендация: латинский ID контекста (`clerk`, `manager`) + кириллический
// `displayName` для UI/слайдов. Любые пользовательские поля пробрасываются как есть
// и доступны хукам через `ctx.testInfo.contexts[name]` (см. §3).
contexts: {
clerk: { url: 'http://localhost/app-clerk/ru_RU', displayName: 'Кладовщик' },
manager: { url: 'http://localhost/app-manager/ru_RU', displayName: 'Менеджер' },
admin: { url: 'http://localhost/app-admin/ru_RU', displayName: 'Админ' },
},
defaultContext: 'clerk',
// Значения по умолчанию (переопределяются флагами CLI)
timeout: 30000,
retries: 0,
screenshot: 'on-failure', // 'every-step' | 'off'
record: false,
// Дефолтный тег-фильтр. Применяется только если CLI не передал --tags.
// Удобно для сценариев «прогон по умолчанию = smoke», при этом --tags=full
// (или --tags=) с CLI прозрачно перекрывает.
tags: ['smoke'],
// Дефолтный режим изоляции для контекстов, которые сами его не указали
// (config.contexts.<name>.isolation). См. §8.
isolation: 'tab', // 'tab' | 'window'
// Allure severity policy (опционально). Маппинг наоборот: уровень → [теги].
// Резолв см. §9 «Авто-эмиссия label-ов».
severity: {
critical: ['smoke', 'multi-context'],
minor: ['recording'],
// blocker / trivial — необязательны, можно опустить
},
defaultSeverity: 'normal',
};
```
**Упрощённая форма** (один контекст, без именованных):
```js
export default {
url: 'http://localhost/app/ru_RU',
timeout: 30000,
};
```
### Валидация файла конфигурации
`severity` валидируется при загрузке:
- ключи — только из `blocker | critical | normal | minor | trivial`;
- значение каждого ключа — массив тегов;
- тег не может одновременно состоять в двух уровнях severity (явная ошибка с указанием конфликта);
- `defaultSeverity` — из стандартного набора.
При нарушении любого правила движок выводит сообщение с указанием конфликта и завершается с ненулевым кодом до запуска тестов.
Кириллица в ID контекстов работает, но смешанный регистр снижает читаемость кода (`testInfo.contexts.кладовщик.displayName` рядом с `testInfo.contexts.clerk.displayName`). Рекомендуем разделять технический ID и человекочитаемое имя.
Флаги CLI всегда переопределяют значения из файла конфигурации.
---
## 8. Контексты
### Механизм: Playwright BrowserContext
Один процесс браузера (`chromium.launch()`), несколько изолированных контекстов. Каждый контекст — отдельная сессия (куки, авторизация, состояние страницы).
```
browser (один процесс chromium)
├─ BrowserContext "кладовщик" → page → http://localhost/app-clerk/ru_RU
├─ BrowserContext "менеджер" → page → http://localhost/app-mgr/ru_RU
└─ BrowserContext "админ" → page → http://localhost/app-admin/ru_RU
```
Преимущества:
- **Мгновенное переключение** между пользователями (смена активного `page`).
- **Состояние сохраняется** — переключились на менеджера и обратно, у кладовщика все формы остались открытыми.
- **Нет переподключений** — каждая сессия живёт независимо.
- **Один процесс** — экономия ресурсов по сравнению с несколькими браузерами.
### Одиночный контекст (по умолчанию)
Большинство тестов. Один BrowserContext, один пользователь. Тест получает плоский `ctx` со всем API.
```js
export const context = 'manager'; // необязательно, иначе defaultContext
export default async function({ clickElement, fillFields, }) { }
```
### Порядок выполнения и переключение контекста
Движок НЕ группирует тесты по контексту. Порядок выполнения — алфавитный по полному относительному пути файла (плюс порядок экспорта внутри файла). Для каждого теста:
1. Через `ensureContext(name)` создаются BrowserContext-ы, упомянутые в `t.context` / `t.contexts` (если ещё не созданы).
2. `setActiveContext(primaryContext)` — активный контекст = первый объявленный (для single — `t.context || defaultContext`, для multi — `t.contexts[0]`).
3. После теста встроенный сброс пробегает по всем использованным контекстам.
Контексты живут между тестами: переключение через `setActiveContext` — дешёвое, повторный вход в 1С не требуется. Закрываются явно (`closeContext`) или финальной очисткой движка перед закрытием браузера.
### Мульти-контекст (процессные тесты)
```js
export const contexts = ['clerk', 'manager'];
export default async function({ clerk, manager, step, assert }) { }
```
Каждый именованный контекст — полноценный scoped-объект API со своим `page`. Тест оркестрирует переключение между пользователями. Состояние каждого пользователя сохраняется между переключениями:
```js
await step('Кладовщик создаёт документ', async () => {
await clerk.openCommand('Приходные накладные');
await clerk.clickElement('Создать');
await clerk.fillFields({ 'Контрагент': 'ООО Поставщик' });
await clerk.clickElement('Записать');
// кладовщик стоит на форме документа
});
await step('Менеджер утверждает', async () => {
await manager.navigateSection('Согласование');
await manager.clickElement('Утвердить');
});
await step('Кладовщик проверяет статус', async () => {
// страница кладовщика ТА ЖЕ — форма открыта, навигация не нужна
const state = await clerk.getFormState();
assert.equal(state.fields['Статус']?.value, 'Утверждён');
});
```
### Публичный контекстный API
| Метод | Назначение |
|-------|-----------|
| `createContext(name, url, { isolation, extensionPath })` | Создаёт BrowserContext и переходит по URL. |
| `setActiveContext(name)` | Переключает активный слот; при активной записи дописывает последние кадры старой страницы и переподключает screencast. |
| `closeContext(name)` | Выход из 1С + закрытие (`page` для `tab`, `BrowserContext` для `window`), удаляет из реестра. Бросает исключение, если `name === active`. |
| `listContexts()` / `hasContext(name)` / `getActiveContext()` | Только для чтения. |
### Режимы изоляции
Поле `isolation` задаётся в двух местах:
- **На уровне контекста:** `config.contexts.<name>.isolation` — приоритет 1.
- **На уровне файла конфигурации:** `config.isolation` — применяется к контекстам, у которых своего значения нет. По умолчанию `'tab'`.
| Режим | Реализация | Окна | Cookies | 1С-расширение |
|-------|-----------|------|---------|---------------|
| `'tab'` (default) | `launchPersistentContext` + `newPage()` per context | 1 окно, N вкладок | общие по path | загружается надёжно |
| `'window'` | `chromium.launch()` + `newContext()` per context | N окон | полная изоляция | может не загружаться |
Смешивать режимы в одном прогоне нельзя — `createContext` бросает явную ошибку. То есть `config.isolation` фактически становится режимом всего прогона, если хотя бы один контекст явно не переопределил его на тот же режим.
### Закрытие неактивных контекстов
`closeContext(name)` нельзя вызвать на активном контексте — будет исключение. В scoped API это естественно: вызывать `manager.closeContext('clerk')` (scoped-обёртка сначала переключает активный на `manager`, потом закрывает `clerk`). Если контекст лишний (роль больше не нужна в рамках теста / прогона) — закрывайте его сразу: освобождает лицензию платформы и снимает нагрузку со следующих тестов.
---
## 9. Отчёты
### JSON (нативный, по умолчанию)
```json
{
"runner": "web-test",
"url": "http://localhost/app/ru_RU",
"startedAt": "2026-04-05T10:00:00.000Z",
"finishedAt": "2026-04-05T10:05:30.000Z",
"duration": 330.0,
"summary": {
"total": 25,
"passed": 23,
"failed": 1,
"skipped": 1
},
"tests": [
{
"name": "CRUD справочника Контрагенты",
"file": "02-catalog-crud.test.mjs",
"tags": ["smoke", "crud"],
"contexts": ["clerk"],
"severity": "critical",
"status": "passed",
"start": 1712345678000,
"stop": 1712345690300,
"duration": 12.3,
"attempts": 1,
"steps": [
{
"name": "Открыть список",
"start": 1712345678000,
"stop": 1712345679200,
"status": "passed",
"steps": []
}
],
"output": "Элемент найден в списке",
"error": null,
"screenshot": null,
"video": null
},
{
"name": "Обязательное поле",
"file": "10-validation.test.mjs",
"tags": ["validation"],
"contexts": ["clerk"],
"status": "failed",
"duration": 8.1,
"attempts": 2,
"steps": [
{
"name": "Сохранить пустую форму",
"start": 1712345700000,
"stop": 1712345708100,
"status": "failed",
"error": "Ожидалось модальное окно ошибки, но форма сохранилась"
}
],
"output": "",
"error": {
"message": "Ожидалось модальное окно ошибки, но форма сохранилась",
"step": "Сохранить пустую форму",
"screenshot": "error-shot-10.png"
},
"screenshot": "error-shot-10.png"
}
]
}
```
### Allure (`--format=allure --report-dir=allure-results/`)
Отдельные JSON-файлы для каждого теста в каталоге `allure-results/`:
```json
{
"uuid": "сгенерированный-uuid",
"name": "CRUD справочника",
"fullName": "02-catalog-crud.test.mjs",
"status": "passed",
"stage": "finished",
"start": 1712345678000,
"stop": 1712345690300,
"labels": [
{ "name": "tag", "value": "smoke" },
{ "name": "tag", "value": "crud" },
{ "name": "suite", "value": "root" },
{ "name": "severity", "value": "critical" }
],
"steps": [
{
"name": "Открыть список",
"status": "passed",
"start": 1712345678000,
"stop": 1712345679200,
"steps": []
}
],
"attachments": [
{
"name": "Скриншот при падении",
"source": "uuid-attachment.png",
"type": "image/png"
}
]
}
```
Скриншоты/видео копируются в `allure-results/` с уникальными именами.
#### Авто-эмиссия меток
Движок всегда заполняет следующие метки (`labels`):
- **`tag`** — по одному на каждый элемент `mod.tags[]`. Готовая фильтрация в Allure-отчёте без дополнительной разметки.
- **`suite`** — `dirname(t.filePath)`. Тесты в корне `testDir` идут под `'root'`, тесты в подкаталоге `sales/` — под `'sales'`. Это даёт левую группировку отчёта без ручной разметки.
- **`severity`** — резолв в порядке приоритета:
1. `export const severity = 'critical'` в самом тесте, **если значение валидное** (одно из `blocker | critical | normal | minor | trivial`). Если экспорт задан, но значение невалидное — пункт пропускается и идём в (3); резолв через теги (пункт 2) при этом **не выполняется** (хотел бы автор иначе — он бы не объявлял `severity`).
2. Иначе **максимальный ранг** среди тегов теста (стандартные имена `blocker | critical | normal | minor | trivial` напрямую, либо через `config.severity`-маппинг).
3. Иначе `config.defaultSeverity` или `'normal'`.
Ранги: `blocker(5) > critical(4) > normal(3) > minor(2) > trivial(1)`. Выбор по максимуму не зависит от порядка тегов в `mod.tags`.
Пример: `tags: ['smoke', 'recording']` + `severity: { critical: ['smoke'], minor: ['recording'] }` → severity = `critical` (5 > 2).
#### Доп. файлы Allure через `<testDir>/_allure/`
Движок ищет каталог `_allure/` рядом с тестами и копирует все его файлы в `reportDir` перед генерацией отчёта. Конвенция для статичной настройки Allure, для которой нет места внутри JSON-файла теста:
| Файл | Назначение |
|------|-----------|
| `categories.json` | Классификация падений по regex (группировка failed-тестов в виджете Categories — «timeout», «license-flake», «1C modal» и т.п.). |
| `environment.properties` | `key=value` строки в виджет Environment (URL, версия 1С, ветка git, номер сборки). Часто формируется динамически из `prepare()`. |
| `executor.json` | CI/CD-метаданные (Jenkins URL, GitHub run-id и т.п.). |
Подчёркивание в имени — параллель `_hooks.mjs` (инфраструктура, не тест). Сборщик тестов пропускает каталог `_allure/` по общему правилу (`startsWith('_')`). Если каталога нет — ничего не происходит, отчёт собирается обычным образом.
Пример `categories.json` (минимальный):
```json
[
{ "name": "Timeout", "messageRegex": "Timeout \\(\\d+ms\\)" },
{ "name": "Assertion", "messageRegex": "(Expected|AssertionError).*" }
]
```
### JUnit XML (`--format=junit`)
```xml
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="web-test" tests="25" failures="1" skipped="1" time="330.0">
<testsuite name="tests/myapp" tests="25" failures="1" skipped="1">
<testcase name="CRUD справочника" classname="02-catalog-crud.test.mjs" time="12.3"/>
<testcase name="Обязательное поле" classname="10-validation.test.mjs" time="8.1">
<failure message="Ожидалось модальное окно ошибки, но форма сохранилась">
Стек вызовов…
</failure>
<system-out>Скриншот: error-shot-10.png</system-out>
</testcase>
</testsuite>
</testsuites>
```
---
## 10. Консольный вывод
```
web-test — http://localhost/app/ru_RU
Запуск 25 тестов из tests/myapp/
✓ Навигация по разделам (2.1s)
✓ CRUD справочника Контрагенты (12.3s)
├ Открыть список (1.2s)
├ Создать элемент (8.0s)
└ Проверить в списке (3.1s)
✗ Обязательное поле (8.1s)
├ Открыть форму (2.0s)
└ ✗ Сохранить пустую форму (6.1s)
Ожидалось модальное окно ошибки, но форма сохранилась
скриншот: error-shot-10.png
○ Составной тип (skip: не реализовано)
23 passed, 1 failed, 1 skipped (2m 0.5s)
```
Для passed-тестов выводится одна строка `✓ name (duration)`. Шаги печатаются только для упавших — после строки `✗`, с отступом, плюс сообщение ошибки и путь к скриншоту. Полная картина по шагам — в JSON-отчёте (`--report=…`).
---
## 11. Скриншоты и видео
### Стратегия скриншотов
| Стратегия | Поведение |
|-----------|----------|
| `on-failure` (по умолчанию) | Скриншот при падении теста, прикрепляется к ошибке. |
| `every-step` | Скриншот в конце каждого `step()`, плюс при падении. |
| `off` | Без автоматических скриншотов. |
Скриншоты сохраняются в каталог отчёта по шаблону `{индекс-теста}-{имя-шага}.png`. В JSON-отчёте — путь относительно каталога отчёта.
### Видеозапись
При включённом `--record`:
- `startRecording()` перед каждым тестом.
- `stopRecording()` после каждого теста.
- Видео сохраняется как `{индекс-теста}-{имя-теста}.mp4`.
- Прикрепляется к отчёту (Allure: вложение видео).
Подробности по записи (overlays, captions, narration) — см. [web-test-recording-guide.md](web-test-recording-guide.md).
---
## 12. Сброс состояния
Встроенный механизм, выполняется после `afterEach``teardown`) каждого теста. Псевдокод и условие выхода — в §6 «Встроенный сброс состояния».
Для мульти-контекстных тестов сброс пробегает по всем живым контекстам, использованным тестом.
Гарантирует, что каждый тест стартует с чистого рабочего стола, независимо от того, как завершился предыдущий (падение, таймаут, ошибка утверждения).
---
## 13. Параметризация
```js
export const name = 'Заполнение поля {type}';
export const params = [
{ type: 'String', field: 'Наименование', value: 'Тест' },
{ type: 'Number', field: 'Цена', value: '100.50' },
{ type: 'Date', field: 'ДатаПоступления', value: '01.01.2024' },
{ type: 'Boolean',field: 'Активен', value: true },
];
export default async function({ fillFields, getFormState, assert }, { type, field, value }) {
await fillFields({ [field]: value });
const state = await getFormState();
assert.equal(state.fields[field]?.value, String(value));
}
```
Параметры разворачиваются в отдельные тесты на этапе discovery:
- Имя теста формируется подстановкой через шаблон `{key}` в `mod.name`; если шаблона нет — суффикс `[index]`.
- Тест получает `param` вторым аргументом (`default(ctx, param)`).
- В отчётах каждый набор — отдельная запись со своим `name` и `param` в `testInfo`.
- `ctx.testInfo.param` доступен в теле теста и хуках.
---
## 14. Обнаружение тестов
`testDir` (первый позиционный аргумент после URL) — каталог, в котором живут тесты. Сборщик рекурсивно обходит дерево и собирает файлы по правилам ниже.
```
tests/myapp/
_hooks.mjs # пропускается (префикс '_')
_allure/ # пропускается (префикс '_')
webtest.config.mjs # пропускается (не *.test.mjs)
sales/
01-order-create.test.mjs
02-order-post.test.mjs
warehouse/
01-receipt.test.mjs
```
### Правила
| Аспект | Поведение |
|--------|-----------|
| Обход | Рекурсивный; файлы и каталоги, имя которых начинается на `_` или `.`, пропускаются |
| Шаблон имени | Только `*.test.mjs` |
| Порядок | Сортировка по полному относительному пути (`sales/01` идёт до `warehouse/01`) |
| `file` в отчёте | `relative(testDir, file)` с разделителем `/`, например `sales/01-order-create.test.mjs` |
| Фильтр по пути с CLI | `node run.mjs test tests/myapp/sales/` запустит только подкаталог |
| Конкретный файл | `node run.mjs test tests/myapp/sales/01-order-create.test.mjs` |
### Чего НЕТ (сознательное упрощение)
- **`_hooks.mjs` на уровне подкаталога.** Движок ищет `_hooks.mjs` только в корне `testDir`. Подкаталоги свои хуки не получают.
- **`webtest.config.mjs` на уровне подкаталога.** Тоже только в корне.
- **Многоуровневой Suite-разметки из дерева каталогов.** Allure-метка `suite` строится только по первому уровню (`dirname(filePath)`); более глубокую группировку делайте через `tags`.
- **Контекста по умолчанию на уровне подкаталога.** Каждый тест объявляет `context` / `contexts` сам; от пути контексты не наследуются.
### Конвенции
1. **Папки — для организации**, не для механики. Общая подготовка — в глобальном `_hooks.mjs.beforeAll` или в `setup` / `teardown` конкретного теста.
2. **Группировку в отчётах** делайте через `tags: ['sales']`, не через путь. Это даёт фильтрацию (`--tags=sales`) и работает в Allure/JUnit без дополнительной разметки.
3. **«Запустить только sales»** — двумя путями: `tests/myapp/sales/` (по каталогу) или `--tags=sales` (по тегу).
4. **Сортировка по полному пути** означает, что `warehouse/01-x` запустится ПОСЛЕ `sales/02-y`. Для строгого глобального порядка используйте 3-значные префиксы (`010-`/`020-`/…) либо явные теги-фазы.
---
## 15. Ошибки и трассировка
### Авто-обнаружение 1С-ошибок
Все ACTION_FNS (`clickElement`, `fillFields`, `fillField`, `selectValue`, `fillTableRow`, `deleteTableRow`, `openCommand`, `navigateSection`, `navigateLink`, `openFile`, `closeForm`, `filterList`, `unfilterList`) обёрнуты. После каждого вызова:
1. Проверяется `state.errors.modal` / `balloon`.
2. Если есть — делается скриншот (до того, как `fetchErrorStack` закроет модалку).
3. Для модальных ошибок вызывается `fetchErrorStack` (две стратегии — Path 1 для платформенных исключений с кнопкой «Открыть отчёт», Path 2 для `ВызватьИсключение` через гамбургер-меню → О программе → Информация для тех. поддержки; см. [web-test-guide.md](web-test-guide.md)).
4. Бросается исключение со структурированным `err.onecError`:
```js
err.onecError = {
step, // имя действия (например 'clickElement')
args, // аргументы, с которыми вызывалось
errors, // { modal?, balloon? }
formState, // снапшот getFormState
stack, // { raw, entries: [{ location, code }], timestamp } | null
screenshot, // путь к скриншоту
};
```
В отчёте это превращается в `error.onecError.stack` для упавшего теста. Разбор причин падения и категории — см. §16.
### Платформенные модальные диалоги
`getFormState()` возвращает `platformDialogs` — массив платформенных диалогов (About, Support Info, Error Report). `closeForm()` закрывает их. `dismissPendingErrors()` чистит ожидающие модалки автоматически (вызывается перед каждым ACTION_FN, плюс в встроенном сбросе после теста).
Модальное окно платформенной ошибки сначала рендерится в переходном состоянии (~1 с), затем перерисовывается в стабильное. `fetchErrorStack` ждёт 1.5 с и перепроверяет `hasReport` перед выбором стратегии.
### Таймауты
- Глобальный таймаут теста: `mod.timeout` или `config.timeout` или CLI `--timeout=ms`.
- Таймаут срабатывает на уровне теста (`testFn()` + `setup` + `teardown`), не на уровне отдельного `step` или action.
- При таймауте: текущий step помечается failed, бросается ошибка с сообщением `Timeout (<N>ms)`, далее запускается `afterEach` и встроенный сброс.
### Повторы
При `--retry=N` (или `config.retries`) упавший тест повторяется до `1 + N` раз. Для каждой попытки:
- `beforeEach` / `setup` / `default` / `teardown` / `afterEach` + встроенный сброс выполняются заново.
- `ctx.testInfo.attempt` инкрементируется.
- В отчёте фиксируется `attempts` — фактически выполнено попыток.
- Считается passed, если последняя попытка зелёная; иначе failed.
`beforeAll` / `afterAll` / `prepare` / `cleanup` / `afterOpenContext` / `beforeCloseContext` не повторяются (это жизненный цикл всего прогона или контекста, не теста).
---
## 16. Анализ результатов
### Что лежит в записи об упавшем тесте
JSON-отчёт (`tests[]`, полная структура — §9) для каждого падения содержит:
- `error.message` — текст исключения.
- `error.step` — имя шага, на котором упало.
- `error.screenshot` — путь к скриншоту падения (если стратегия скриншотов не `off`).
- `error.onecError` (только для 1С-исключений) — структура с полями: `step` (имя действия, например `clickElement`), `args` (аргументы вызова), `errors` (модальное окно или balloon), `formState` (снимок формы на момент ошибки), `stack` — платформенный стек вызовов 1С с `entries[{location, code}]`.
- `steps[]` — пошаговая разбивка с метками времени, у каждого шага свой `status` и `error`.
В Allure-отчёте те же данные лежат в `statusDetails` (текст ошибки и трассировка), скриншоты и видео — во вложениях, автоматическая группировка по причинам — через `categories.json` (§9).
### Типовые причины падений
Большинство падений на 1С-стенде сводится к трём причинам, и их полезно различать при разборе отчёта:
- **Ошибка в тесте** — селектор не нашёл элемент, ожидание не сошлось, гонка без точки синхронизации. Признаки: падение стабильно повторяется на одном и том же шаге; после правки теста воспроизводимость исчезает. Действие — изменить тест.
- **Ошибка в прикладном решении** — реально воспроизведённое некорректное поведение конфигурации. Признаки: упал шаг, имитирующий пользовательскую операцию; в `error.onecError.stack` есть платформенный стек вызовов 1С с указанием на код решения. Действие — передать разработчику конфигурации, тест править не нужно.
- **Сбой стенда** — таймаут Apache, форма входа не загрузилась, не хватило веб-лицензий. Признаки: падение на навигации или входе; от прогона к прогону падает «то одно, то другое», без связи с содержанием теста. Действие — править инфраструктуру (`prepare()`, очистка сессий, идемпотентность хуков), не тесты.
`categories.json` Allure (§9) удобно настраивать именно под эти три категории — regex по `error.message` уже даёт первичную классификацию в виджете Categories.
---
## 17. Глоссарий
| Термин | Определение |
|--------|-------------|
| **testDir** | Каталог тестов, переданный позиционным аргументом движку. Корень для discovery, `_hooks.mjs`, `webtest.config.mjs`, `_allure/`. |
| **Context (BrowserContext)** | Изолированная сессия Playwright. Куки/состояние/страница независимы. В рамках одного теста используется один или несколько контекстов. |
| **Active context** | Контекст, на котором сейчас оперируют функции browser-API. Переключается `setActiveContext`. |
| **Primary context** | Контекст, активный на входе в тест. Декларация (`mod.context` или `mod.contexts[0]`). Зафиксирован в `testInfo.primaryContext`. |
| **Default context** | Контекст из `config.defaultContext` (или единственный URL в упрощённой конфигурации). Используется, если тест не указал `context` / `contexts`. |
| **Scoped API** | Объект на `ctx.<name>` в мульти-контекстных тестах — обёртки browser-функций, авто-переключающие контекст перед каждым вызовом. |
| **Action function (ACTION_FN)** | Browser-функция, обёрнутая авто-обнаружением 1С-ошибок. Список — в §3. |
| **Step** | Логический блок внутри теста, обёрнутый `step(name, fn)`. Маппится на Allure-step, попадает в `report.tests[].steps[]`. |
| **Reset state** | Встроенная пост-тестовая очистка: `dismissPendingErrors` + закрытие всех открытых форм до рабочего стола. Выполняется после `afterEach`. |
| **hookArgs** | Массив строк, переданных в `prepare` / `cleanup` после CLI-разделителя `--`. Движком не интерпретируются. |
| **Severity** | Уровень критичности теста (`blocker / critical / normal / minor / trivial`) для Allure. Резолвится из `mod.severity`, тегов, `config.severity`, `config.defaultSeverity`. |
---
## См. также
- [web-test-guide.md](web-test-guide.md) — browser API (`clickElement`, `getFormState`, `readTable`, …) и интерактивный режим.
- [web-test-recording-guide.md](web-test-recording-guide.md) — видеозапись, captions, narration, overlays.
- [web-test-regression-guide.md](web-test-regression-guide.md) — пользовательский гайд (на русском, с быстрым стартом).
- `/web-test` skill — `.claude/skills/web-test/SKILL.md`, `regress.md` (рабочая шпаргалка для модели).
@@ -34,7 +34,7 @@
</dataSet>
<calculatedField>
<dataPath>ИмяРесурса</dataPath>
<expression>&quot;&quot;</expression>
<expression>""</expression>
<title xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
@@ -42,7 +42,7 @@
</dataSet>
<calculatedField>
<dataPath>ИмяРесурса</dataPath>
<expression>&quot;&quot;</expression>
<expression>""</expression>
<title xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
@@ -34,7 +34,7 @@
</dataSet>
<calculatedField>
<dataPath>ИмяРесурса</dataPath>
<expression>&quot;&quot;</expression>
<expression>""</expression>
<title xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
</dataSource>
<dataSet xsi:type="DataSetQuery">
<name>DS</name>
<field xsi:type="DataSetFieldField">
<dataPath>Контрагент</dataPath>
<field>Контрагент</field>
<title xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Контрагент</v8:content>
</v8:item>
<v8:item>
<v8:lang>en</v8:lang>
<v8:content>Counterparty</v8:content>
</v8:item>
</title>
<role>
<dcscom:dimension>true</dcscom:dimension>
</role>
<valueType>
<v8:Type>xs:string</v8:Type>
<v8:StringQualifiers>
<v8:Length>100</v8:Length>
<v8:AllowedLength>Variable</v8:AllowedLength>
</v8:StringQualifiers>
</valueType>
<appearance>
<dcscor:item xsi:type="dcsset:SettingsParameterValue">
<dcscor:parameter>Формат</dcscor:parameter>
<dcscor:value xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>ЧДЦ=2</v8:content>
</v8:item>
<v8:item>
<v8:lang>en</v8:lang>
<v8:content>NFD=2</v8:content>
</v8:item>
</dcscor:value>
</dcscor:item>
</appearance>
</field>
<dataSource>ИсточникДанных1</dataSource>
<query>ВЫБРАТЬ 1 КАК Контрагент</query>
</dataSet>
<parameter>
<name>Период</name>
<title xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Период</v8:content>
</v8:item>
<v8:item>
<v8:lang>en</v8:lang>
<v8:content>Period</v8:content>
</v8:item>
</title>
<valueType>
<v8:Type>xs:dateTime</v8:Type>
<v8:DateQualifiers>
<v8:DateFractions>DateTime</v8:DateFractions>
</v8:DateQualifiers>
</valueType>
</parameter>
<settingsVariant>
<dcsset:name>Основной</dcsset:name>
<dcsset:presentation xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Основной</v8:content>
</v8:item>
</dcsset:presentation>
<dcsset:settings xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows">
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
</settingsVariant>
</DataCompositionSchema>
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
</dataSource>
<dataSet xsi:type="DataSetQuery">
<name>DS</name>
<field xsi:type="DataSetFieldField">
<dataPath>П</dataPath>
<field>П</field>
</field>
<field xsi:type="DataSetFieldField">
<dataPath>Имя</dataPath>
<field>Имя</field>
</field>
<dataSource>ИсточникДанных1</dataSource>
<query>ВЫБРАТЬ
1 КАК П,
&quot;literal&quot; КАК Имя</query>
</dataSet>
<parameter>
<name>Период</name>
<title xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Период</v8:content>
</v8:item>
</title>
<valueType>
<v8:Type>xs:dateTime</v8:Type>
<v8:DateQualifiers>
<v8:DateFractions>DateTime</v8:DateFractions>
</v8:DateQualifiers>
</valueType>
</parameter>
<settingsVariant>
<dcsset:name>Основной</dcsset:name>
<dcsset:presentation xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Основной</v8:content>
</v8:item>
</dcsset:presentation>
<dcsset:settings xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows">
<dcsset:selection>
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
</settingsVariant>
</DataCompositionSchema>
@@ -0,0 +1,13 @@
{
"name": "NO-OP modify-field на отсутствующее поле — файл не трогаем (regression от XML round-trip бага)",
"setup": "fixture:roundtrip-base",
"params": {
"templatePath": "Template.xml",
"operation": "modify-field",
"value": "НесуществующееПоле [Заголовок]"
},
"expect": {
"stdoutContains": "No changes -- file untouched"
},
"idempotent": true
}
@@ -0,0 +1,10 @@
{
"name": "modify-field сохраняет en/uk title (multi-lang) при ru-override",
"setup": "fixture:multilang-base",
"params": {
"templatePath": "Template.xml",
"operation": "modify-field",
"value": "Контрагент [Имя контрагента]"
},
"idempotent": true
}
@@ -0,0 +1,10 @@
{
"name": "modify-parameter сохраняет en title (multi-lang) при ru-override",
"setup": "fixture:multilang-base",
"params": {
"templatePath": "Template.xml",
"operation": "modify-parameter",
"value": "Период [Новый период]"
},
"idempotent": true
}
@@ -0,0 +1,13 @@
{
"name": "modify-field сохраняет неизвестные дочерние элементы (<appearance> со всеми вложениями) — структура из ERP",
"setup": "fixture:multilang-base",
"params": {
"templatePath": "Template.xml",
"operation": "modify-field",
"value": "Контрагент @dimension"
},
"expect": {
"stdoutContains": "Field \"Контрагент\" modified"
},
"idempotent": true
}
@@ -0,0 +1,10 @@
{
"name": "Многострочный xmlns корня сохраняется после успешной операции",
"setup": "fixture:roundtrip-base",
"params": {
"templatePath": "Template.xml",
"operation": "set-query",
"value": "DS=ВЫБРАТЬ 2 КАК П, &quot;new&quot; КАК Имя"
},
"idempotent": true
}
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -65,10 +72,10 @@
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -53,10 +60,10 @@
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -41,10 +48,10 @@
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -52,10 +59,10 @@
</dcsset:filter>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -43,10 +50,10 @@
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -40,10 +47,10 @@
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -67,10 +74,10 @@
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -73,10 +80,10 @@
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -27,7 +34,7 @@
</dcsset:presentation>
<dcsset:settings xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows">
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
<dcsset:item xsi:type="dcsset:SelectedItemField">
<dcsset:field>Поле1</dcsset:field>
</dcsset:item>
@@ -37,10 +44,10 @@
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -38,10 +45,10 @@
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -42,10 +49,10 @@
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -36,10 +43,10 @@
</dcsset:conditionalAppearance>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -44,7 +51,7 @@
</dcsset:selection>
<dcsset:conditionalAppearance>
<dcsset:item>
<dcsset:selection />
<dcsset:selection/>
<dcsset:filter>
<dcsset:item xsi:type="dcsset:FilterItemComparison">
<dcsset:left xsi:type="dcscor:Field">ПараметрыДанных.ПорядокОкругления</dcsset:left>
@@ -65,7 +72,7 @@
</dcsset:appearance>
</dcsset:item>
<dcsset:item>
<dcsset:selection />
<dcsset:selection/>
<dcsset:filter>
<dcsset:item xsi:type="dcsset:FilterItemGroup">
<dcsset:groupType>OrGroup</dcsset:groupType>
@@ -96,10 +103,10 @@
</dcsset:conditionalAppearance>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
</dataSource>
<dataSet xsi:type="DataSetQuery">
<name>DS</name>
<field xsi:type="DataSetFieldField">
<dataPath>П</dataPath>
<field>П</field>
</field>
<field xsi:type="DataSetFieldField">
<dataPath>Имя</dataPath>
<field>Имя</field>
</field>
<dataSource>ИсточникДанных1</dataSource>
<query>ВЫБРАТЬ
1 КАК П,
&quot;literal&quot; КАК Имя</query>
</dataSet>
<parameter>
<name>Период</name>
<title xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Период</v8:content>
</v8:item>
</title>
<valueType>
<v8:Type>xs:dateTime</v8:Type>
<v8:DateQualifiers>
<v8:DateFractions>DateTime</v8:DateFractions>
</v8:DateQualifiers>
</valueType>
</parameter>
<settingsVariant>
<dcsset:name>Основной</dcsset:name>
<dcsset:presentation xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Основной</v8:content>
</v8:item>
</dcsset:presentation>
<dcsset:settings xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows">
<dcsset:selection>
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
</settingsVariant>
</DataCompositionSchema>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -56,10 +63,10 @@
</dcsset:dataParameters>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -67,10 +74,10 @@
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -74,10 +81,10 @@
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -43,10 +50,10 @@
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -53,10 +60,10 @@
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -40,10 +47,10 @@
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -65,10 +72,10 @@
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -64,10 +71,10 @@
</dcsset:item>
</dcsset:groupItems>
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
<dcsset:item xsi:type="dcsset:SelectedItemField">
<dcsset:field>Счет</dcsset:field>
</dcsset:item>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -44,10 +51,10 @@
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -33,10 +40,10 @@
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -26,10 +33,10 @@
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
</dataSource>
<dataSet xsi:type="DataSetQuery">
<name>DS</name>
<field xsi:type="DataSetFieldField">
<dataPath>Контрагент</dataPath>
<field>Контрагент</field>
<title xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Имя контрагента</v8:content>
</v8:item>
<v8:item>
<v8:lang>en</v8:lang>
<v8:content>Counterparty</v8:content>
</v8:item>
</title>
<role>
<dcscom:dimension>true</dcscom:dimension>
</role>
<valueType>
<v8:Type>xs:string</v8:Type>
<v8:StringQualifiers>
<v8:Length>100</v8:Length>
<v8:AllowedLength>Variable</v8:AllowedLength>
</v8:StringQualifiers>
</valueType>
<appearance>
<dcscor:item xsi:type="dcsset:SettingsParameterValue">
<dcscor:parameter>Формат</dcscor:parameter>
<dcscor:value xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>ЧДЦ=2</v8:content>
</v8:item>
<v8:item>
<v8:lang>en</v8:lang>
<v8:content>NFD=2</v8:content>
</v8:item>
</dcscor:value>
</dcscor:item>
</appearance>
</field>
<dataSource>ИсточникДанных1</dataSource>
<query>ВЫБРАТЬ 1 КАК Контрагент</query>
</dataSet>
<parameter>
<name>Период</name>
<title xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Период</v8:content>
</v8:item>
<v8:item>
<v8:lang>en</v8:lang>
<v8:content>Period</v8:content>
</v8:item>
</title>
<valueType>
<v8:Type>xs:dateTime</v8:Type>
<v8:DateQualifiers>
<v8:DateFractions>DateTime</v8:DateFractions>
</v8:DateQualifiers>
</valueType>
</parameter>
<settingsVariant>
<dcsset:name>Основной</dcsset:name>
<dcsset:presentation xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Основной</v8:content>
</v8:item>
</dcsset:presentation>
<dcsset:settings xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows">
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
</settingsVariant>
</DataCompositionSchema>
@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
</dataSource>
<dataSet xsi:type="DataSetQuery">
<name>DS</name>
<field xsi:type="DataSetFieldField">
<dataPath>Контрагент</dataPath>
<field>Контрагент</field>
<title xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Контрагент</v8:content>
</v8:item>
<v8:item>
<v8:lang>en</v8:lang>
<v8:content>Counterparty</v8:content>
</v8:item>
</title>
<role>
<dcscom:dimension>true</dcscom:dimension>
</role>
<valueType>
<v8:Type>xs:string</v8:Type>
<v8:StringQualifiers>
<v8:Length>100</v8:Length>
<v8:AllowedLength>Variable</v8:AllowedLength>
</v8:StringQualifiers>
</valueType>
<appearance>
<dcscor:item xsi:type="dcsset:SettingsParameterValue">
<dcscor:parameter>Формат</dcscor:parameter>
<dcscor:value xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>ЧДЦ=2</v8:content>
</v8:item>
<v8:item>
<v8:lang>en</v8:lang>
<v8:content>NFD=2</v8:content>
</v8:item>
</dcscor:value>
</dcscor:item>
</appearance>
</field>
<dataSource>ИсточникДанных1</dataSource>
<query>ВЫБРАТЬ 1 КАК Контрагент</query>
</dataSet>
<parameter>
<name>Период</name>
<title xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Новый период</v8:content>
</v8:item>
<v8:item>
<v8:lang>en</v8:lang>
<v8:content>Period</v8:content>
</v8:item>
</title>
<valueType>
<v8:Type>xs:dateTime</v8:Type>
<v8:DateQualifiers>
<v8:DateFractions>DateTime</v8:DateFractions>
</v8:DateQualifiers>
</valueType>
</parameter>
<settingsVariant>
<dcsset:name>Основной</dcsset:name>
<dcsset:presentation xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Основной</v8:content>
</v8:item>
</dcsset:presentation>
<dcsset:settings xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows">
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
</settingsVariant>
</DataCompositionSchema>
@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
</dataSource>
<dataSet xsi:type="DataSetQuery">
<name>DS</name>
<field xsi:type="DataSetFieldField">
<dataPath>Контрагент</dataPath>
<field>Контрагент</field>
<title xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Контрагент</v8:content>
</v8:item>
<v8:item>
<v8:lang>en</v8:lang>
<v8:content>Counterparty</v8:content>
</v8:item>
</title>
<role>
<dcscom:dimension>true</dcscom:dimension>
</role>
<valueType>
<v8:Type>xs:string</v8:Type>
<v8:StringQualifiers>
<v8:Length>100</v8:Length>
<v8:AllowedLength>Variable</v8:AllowedLength>
</v8:StringQualifiers>
</valueType>
<appearance>
<dcscor:item xsi:type="dcsset:SettingsParameterValue">
<dcscor:parameter>Формат</dcscor:parameter>
<dcscor:value xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>ЧДЦ=2</v8:content>
</v8:item>
<v8:item>
<v8:lang>en</v8:lang>
<v8:content>NFD=2</v8:content>
</v8:item>
</dcscor:value>
</dcscor:item>
</appearance>
</field>
<dataSource>ИсточникДанных1</dataSource>
<query>ВЫБРАТЬ 1 КАК Контрагент</query>
</dataSet>
<parameter>
<name>Период</name>
<title xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Период</v8:content>
</v8:item>
<v8:item>
<v8:lang>en</v8:lang>
<v8:content>Period</v8:content>
</v8:item>
</title>
<valueType>
<v8:Type>xs:dateTime</v8:Type>
<v8:DateQualifiers>
<v8:DateFractions>DateTime</v8:DateFractions>
</v8:DateQualifiers>
</valueType>
</parameter>
<settingsVariant>
<dcsset:name>Основной</dcsset:name>
<dcsset:presentation xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Основной</v8:content>
</v8:item>
</dcsset:presentation>
<dcsset:settings xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows">
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
</settingsVariant>
</DataCompositionSchema>
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
</dataSource>
<dataSet xsi:type="DataSetQuery">
<name>DS</name>
<field xsi:type="DataSetFieldField">
<dataPath>П</dataPath>
<field>П</field>
</field>
<field xsi:type="DataSetFieldField">
<dataPath>Имя</dataPath>
<field>Имя</field>
</field>
<dataSource>ИсточникДанных1</dataSource>
<query>DS=ВЫБРАТЬ 2 КАК П, &amp;quot;new&amp;quot; КАК Имя</query>
</dataSet>
<parameter>
<name>Период</name>
<title xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Период</v8:content>
</v8:item>
</title>
<valueType>
<v8:Type>xs:dateTime</v8:Type>
<v8:DateQualifiers>
<v8:DateFractions>DateTime</v8:DateFractions>
</v8:DateQualifiers>
</valueType>
</parameter>
<settingsVariant>
<dcsset:name>Основной</dcsset:name>
<dcsset:presentation xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Основной</v8:content>
</v8:item>
</dcsset:presentation>
<dcsset:settings xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows">
<dcsset:selection>
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
</settingsVariant>
</DataCompositionSchema>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -108,10 +115,10 @@
</dcsset:dataParameters>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -81,10 +88,10 @@
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -39,10 +46,10 @@
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -34,10 +41,10 @@
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -26,10 +33,10 @@
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -77,10 +84,10 @@
</dcsset:item>
</dcsset:groupItems>
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
<dcsset:selection>
@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
@@ -77,10 +84,10 @@
</dcsset:item>
</dcsset:groupItems>
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto" />
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto" />
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
<dcsset:item xsi:type="dcsset:SelectedItemField">
<dcsset:field>Счет</dcsset:field>
</dcsset:item>
+58
View File
@@ -342,6 +342,31 @@ function normalizeContent(text, config) {
// ─── Snapshot comparison ────────────────────────────────────────────────────
// Capture raw byte contents of every file in dir, keyed by relative path.
// Used by idempotency checks to verify byte-equality after a re-run.
function snapshotWorkDirBytes(dir) {
const files = listFilesRecursive(dir);
const map = new Map();
for (const rel of files) {
map.set(rel, readFileSync(join(dir, rel)));
}
return map;
}
// Compare two byte-snapshots. Returns null if identical, else a list of diff lines.
function diffByteSnapshots(before, after) {
const diffs = [];
for (const [rel, b1] of before) {
if (!after.has(rel)) { diffs.push(`removed: ${rel}`); continue; }
const b2 = after.get(rel);
if (b1.length !== b2.length || !b1.equals(b2)) diffs.push(`changed: ${rel} (${b1.length} -> ${b2.length} bytes)`);
}
for (const rel of after.keys()) {
if (!before.has(rel)) diffs.push(`added: ${rel}`);
}
return diffs.length === 0 ? null : diffs;
}
function listFilesRecursive(dir, base = '') {
const result = [];
if (!existsSync(dir)) return result;
@@ -571,6 +596,23 @@ async function runCaseAsync(testCase, opts) {
}
}
}
// Idempotency check: re-run the same script with the same args and assert
// every file in workDir is byte-identical to the first-run output.
if (errors.length === 0 && caseData.idempotent && !workspace.readOnly) {
const before = snapshotWorkDirBytes(workDir);
try {
const execCwd = skillConfig.cwd === 'workDir' ? workDir : undefined;
await execSkillAsync(opts.runtime, scriptPath, args, execCwd);
} catch (e) {
errors.push(`Idempotency rerun failed: exitCode=${e.status}\nstderr: ${(e.stderr || '').substring(0, 300)}`);
}
if (errors.length === 0) {
const after = snapshotWorkDirBytes(workDir);
const diffs = diffByteSnapshots(before, after);
if (diffs) errors.push(`Idempotency: workspace changed on rerun:\n ${diffs.join('\n ')}`);
}
}
}
// Post-run validation (on real output, before cleanup)
@@ -727,6 +769,22 @@ function runCase(testCase, opts) {
}
}
}
// Idempotency check: re-run the same script and assert byte-equality.
if (errors.length === 0 && caseData.idempotent && !workspace.readOnly) {
const before = snapshotWorkDirBytes(workDir);
try {
const execCwd = skillConfig.cwd === 'workDir' ? workDir : undefined;
execSkillRaw(opts.runtime, scriptPath, args, execCwd);
} catch (e) {
errors.push(`Idempotency rerun failed: exitCode=${e.status}\nstderr: ${(e.stderr || '').substring(0, 300)}`);
}
if (errors.length === 0) {
const after = snapshotWorkDirBytes(workDir);
const diffs = diffByteSnapshots(before, after);
if (diffs) errors.push(`Idempotency: workspace changed on rerun:\n ${diffs.join('\n ')}`);
}
}
}
// Post-run validation (on real output, before cleanup)