Compare commits

..
Author SHA1 Message Date
github-actions[bot] fae79a075f Auto-build: codex (python) from 83bb5b6 2026-08-14 16:58:08 +00:00
4106 changed files with 10066 additions and 248990 deletions
-32
View File
@@ -1,32 +0,0 @@
{
"name": "cc-1c-skills",
"interface": {
"displayName": "1C Skills"
},
"plugins": [
{
"name": "1c-skills",
"source": {
"source": "url",
"url": "https://github.com/Nikolay-Shirokov/cc-1c-skills.git",
"ref": "port-codex"
},
"policy": {
"installation": "AVAILABLE"
},
"category": "Development"
},
{
"name": "1c-skills-py",
"source": {
"source": "url",
"url": "https://github.com/Nikolay-Shirokov/cc-1c-skills.git",
"ref": "port-codex-py"
},
"policy": {
"installation": "AVAILABLE"
},
"category": "Development"
}
]
}
-24
View File
@@ -1,24 +0,0 @@
{
"$schema": "https://json.schemastore.org/claude-code-marketplace-manifest.json",
"name": "cc-1c-skills",
"description": "Маркетплейс навыков для разработки на платформе 1С:Предприятие",
"owner": {
"name": "Nikolay Shirokov"
},
"plugins": [
{
"name": "1c-skills",
"source": "./",
"description": "[PowerShell] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент."
},
{
"name": "1c-skills-py",
"source": {
"source": "github",
"repo": "Nikolay-Shirokov/cc-1c-skills",
"ref": "port-claude-code-py"
},
"description": "[Python] То же — для Linux/Mac или когда PowerShell недоступен."
}
]
}
-31
View File
@@ -1,31 +0,0 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "1c-skills",
"description": "[PowerShell] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент.",
"author": {
"name": "Nikolay Shirokov"
},
"homepage": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
"repository": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
"license": "MIT",
"keywords": [
"1c",
"1c-dev",
"cf",
"cfe",
"epf",
"erf",
"metadata",
"configuration",
"extension",
"form",
"report",
"skd",
"data-processor",
"mxl",
"web-client",
"testing",
"test-automation"
],
"skills": "./.claude/skills/"
}
-49
View File
@@ -1,49 +0,0 @@
---
name: cf-init
description: Создать пустую конфигурацию 1С (scaffold XML-исходников). Используй когда нужно начать новую конфигурацию с нуля
argument-hint: <Name> [-Synonym <name>] [-OutputDir src]
allowed-tools:
- Bash
- Read
- Glob
---
# /cf-init — Создание пустой конфигурации 1С
Создаёт scaffold исходников пустой конфигурации 1С: `Configuration.xml`, `Languages/Русский.xml`.
## Параметры и команда
| Параметр | Описание |
|----------|----------|
| `Name` | Имя конфигурации (обязат.) |
| `Synonym` | Синоним (= Name если не указан) |
| `OutputDir` | Каталог для создания (default: `src`) |
| `Version` | Версия конфигурации |
| `Vendor` | Поставщик |
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-init.ps1" -Name "МояКонфигурация"
```
## Примеры
```powershell
# Базовая конфигурация
... -Name МояКонфигурация -Synonym "Моя конфигурация" -OutputDir test-tmp/cf
# С версией и поставщиком
... -Name TestCfg -Synonym "Тестовая" -Version "1.0.0.1" -Vendor "Фирма 1С" -OutputDir test-tmp/cf2
# Другой режим совместимости
... -Name TestCfg -CompatibilityMode Version8_3_27 -OutputDir test-tmp/cf3
```
## Верификация
```
/cf-init TestConfig -OutputDir test-tmp/cf
/cf-info test-tmp/cf — проверить созданное
/cf-validate test-tmp/cf — валидировать
```
-66
View File
@@ -1,66 +0,0 @@
---
name: mxl-compile
description: Компиляция табличного документа (MXL) из JSON-определения. Используй когда нужно создать макет печатной формы
argument-hint: <JsonPath> <OutputPath>
allowed-tools:
- Bash
- Read
- Write
- Glob
---
# /mxl-compile — Компилятор макета из DSL
Принимает компактное JSON-определение макета и генерирует корректный Template.xml для табличного документа 1С. Claude описывает *что* нужно (области, параметры, стили), скрипт обеспечивает *корректность* XML (палитры, индексы, объединения, namespace).
## Использование
```
/mxl-compile <JsonPath> <OutputPath>
```
## Параметры
| Параметр | Обязательный | Описание |
|------------|:------------:|------------------------------------|
| JsonPath | да | Путь к JSON-определению макета |
| OutputPath | да | Путь для генерации Template.xml |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-compile.ps1" -JsonPath "<путь>.json" -OutputPath "<путь>/Template.xml"
```
## Рабочий процесс
1. Написать JSON-определение (Write tool) → файл `.json`
2. Вызвать `/mxl-compile` для генерации Template.xml
3. Вызвать `/mxl-validate` для проверки корректности
4. Вызвать `/mxl-info` для верификации структуры
**Если макет создаётся по изображению** (скриншот, скан печатной формы) — сначала вызвать `/img-grid` для наложения сетки, по ней определить границы колонок и пропорции, затем использовать `"Nx"` ширины + `"page"` для автоматического расчёта размеров.
## JSON-схема DSL
Ниже — компактная структура и ключевые правила, достаточные для типового макета. Полные таблицы полей (все свойства шрифтов, стилей, ячеек), развёрнутый пример и ограничения формата — в **`reference/dsl-spec.md`**; нужны не всегда, читать по необходимости.
Краткая структура:
```
{ columns, page, defaultWidth, columnWidths,
fonts: { name: { face, size, bold, italic, underline, strikeout } },
styles: { name: { font, align, valign, border, borderWidth, wrap, format } },
areas: [{ name, rows: [{ height, rowStyle, cells: [
{ col, span, rowspan, style, param, detail, text, template }
]}]}]
}
```
Ключевые правила:
- `page` — формат страницы (`"A4-landscape"`, `"A4-portrait"` или число). Автоматически вычисляет `defaultWidth` из суммы пропорций `"Nx"`
- `col` — 1-based позиция колонки
- `rowStyle` — автозаполнение пустот стилем (рамки по всей ширине)
- Тип заполнения определяется автоматически: `param` → Parameter, `text` → Text, `template` → Template
- `rowspan` — объединение строк вниз (rowStyle учитывает занятые ячейки)
- `empty` в строке — шорткат для N подряд пустых строк (`{ "empty": 3 }` = три `{}`)
@@ -1,160 +0,0 @@
# Спецификация MXL DSL — JSON-формат описания табличного документа
Компактный JSON-формат для описания макетов табличных документов 1С (SpreadsheetDocument). Используется навыком `/mxl-compile` (JSON → XML).
## Пример
```json
{
"columns": 10,
"defaultWidth": 30,
"columnWidths": { "1": 15, "2-8": 40, "9-10": 50 },
"fonts": {
"default": { "face": "Arial", "size": 10 },
"bold": { "face": "Arial", "size": 10, "bold": true },
"header": { "face": "Arial", "size": 14, "bold": true }
},
"styles": {
"default": {},
"header": { "font": "header", "align": "center" },
"label": { "font": "bold" },
"bordered": { "border": "all" },
"bordered-right": { "border": "all", "align": "right" },
"total-right": { "font": "bold", "border": "top", "align": "right" }
},
"areas": [
{
"name": "Заголовок",
"rows": [
{ "height": 20, "cells": [
{ "col": 1, "span": 10, "style": "header", "param": "ТекстЗаголовка" }
]}
]
},
{
"name": "ШапкаТаблицы",
"rows": [
{ "rowStyle": "bordered", "cells": [
{ "col": 1, "text": "№" },
{ "col": 2, "span": 6, "text": "Наименование" },
{ "col": 9, "text": "Кол-во" },
{ "col": 10, "text": "Сумма" }
]}
]
},
{
"name": "Строка",
"rows": [
{ "rowStyle": "bordered", "cells": [
{ "col": 1, "param": "НомерСтроки" },
{ "col": 2, "span": 6, "param": "Товар", "detail": "Номенклатура" },
{ "col": 9, "style": "bordered-right", "param": "Количество" },
{ "col": 10, "style": "bordered-right", "param": "Сумма" }
]}
]
},
{
"name": "Итого",
"rows": [
{ "cells": [
{ "col": 8, "span": 2, "style": "total-right", "text": "Итого:" },
{ "col": 10, "style": "total-right", "param": "Всего" }
]}
]
}
]
}
```
## Верхний уровень
| Поле | Обяз. | По умолч. | Описание |
|------|:-----:|-----------|----------|
| `columns` | да | — | Количество колонок |
| `page` | нет | — | Формат страницы: `"A4-landscape"` (780), `"A4-portrait"` (540) или число. Автоматически вычисляет `defaultWidth` из суммы пропорций `"Nx"` |
| `defaultWidth` | нет | 10 | Ширина колонок по умолчанию. Игнорируется если задан `page` и все колонки используют `"Nx"` |
| `columnWidths` | нет | `{}` | Ширины колонок. Ключи 1-based: `"1"`, `"3-14"`, `"5,7,9"`. Значения: число (абсолют) или `"Nx"` (множитель от defaultWidth, напр. `"2x"`, `"0.5x"`) |
| `fonts` | нет | — | Именованные шрифты (если не задано, создаётся Arial 10) |
| `styles` | нет | `{}` | Именованные стили |
| `areas` | да | — | Массив именованных областей (порядок = порядок в документе) |
## Шрифты (`fonts.<name>`)
| Поле | По умолч. | Описание |
|------|-----------|----------|
| `face` | `"Arial"` | Имя шрифта |
| `size` | `10` | Размер |
| `bold` | `false` | Жирный |
| `italic` | `false` | Курсив |
| `underline` | `false` | Подчёркнутый |
| `strikeout` | `false` | Зачёркнутый |
Шрифт `"default"` используется когда стиль не указывает шрифт явно. Если не определён, создаётся автоматически (Arial 10).
## Стили (`styles.<name>`)
| Поле | По умолч. | Описание |
|------|-----------|----------|
| `font` | `"default"` | Ссылка на имя шрифта |
| `align` | — | `left`, `center`, `right` |
| `valign` | — | `top`, `center` |
| `border` | — | Стороны рамки: `all`, `top`, `bottom`, `left`, `right`, `none`. Через запятую: `"top,bottom"` |
| `borderWidth` | `"thin"` | Толщина рамки: `thin` (1px) или `thick` (2px) |
| `wrap` | `false` | Перенос текста |
| `format` | — | Формат данных 1С: `"ЧЦ=15; ЧДЦ=2"`, `"ДФ=dd.MM.yyyy"` и т.д. |
## Области (`areas[]`)
| Поле | Обяз. | Описание |
|------|:-----:|----------|
| `name` | да | Имя области для `Макет.ПолучитьОбласть("Имя")` |
| `rows` | да | Массив строк |
## Строки (`rows[]`)
| Поле | По умолч. | Описание |
|------|-----------|----------|
| `height` | — | Высота строки (если не задана, используется авто) |
| `rowStyle` | — | Стиль для ВСЕХ колонок (заполняет пустоты рамками) |
| `cells` | `[]` | Массив ячеек |
| `empty` | — | Количество подряд идущих пустых строк (заменяет N отдельных `{}`) |
Строка без `cells` и `rowStyle` → пустая строка. `{ "empty": 3 }` эквивалентно трём `{}`.
## Ячейки (`cells[]`)
| Поле | Обяз. | По умолч. | Описание |
|------|:-----:|-----------|----------|
| `col` | да | — | Позиция колонки (1-based) |
| `span` | нет | `1` | Объединение по горизонтали (количество колонок) |
| `rowspan` | нет | `1` | Объединение по вертикали (количество строк) |
| `style` | нет | rowStyle | Стиль ячейки (переопределяет rowStyle) |
| `param` | нет | — | Параметр заполнения |
| `detail` | нет | — | Параметр расшифровки (только с `param`) |
| `text` | нет | — | Статический текст |
| `template` | нет | — | Шаблонный текст с `[Параметр]` |
### Тип заполнения
Определяется автоматически по содержимому ячейки:
- `param` → fillType=Parameter
- `template` → fillType=Template
- `text` → fillType=Text
- ничего → без fillType (пустая ячейка или рамка)
## `rowStyle` — автозаполнение
Когда задан `rowStyle`, компилятор создаёт ячейки для ВСЕХ колонок строки. Позиции без явных ячеек заполняются пустыми ячейками с указанным стилем. Это обеспечивает сплошные рамки в табличных строках.
Если в предыдущих строках той же области есть ячейки с `rowspan`, их колонки при автозаполнении пропускаются.
## Ограничения
Текущая версия не поддерживает:
- Множественные наборы колонок (`columnsID`)
- Области типа Columns / Rectangle
- Рисунки (штрихкоды, картинки)
- Фон ячеек
@@ -1,925 +0,0 @@
# mxl-compile v1.14 — Compile 1C spreadsheet from JSON (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[string]$JsonPath,
[Parameter(Mandatory)]
[string]$OutputPath
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Support guard (Ext/ParentConfigurations.bin) ---
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
# read-only configs unless allowed. Trigger = bin present; reaction from
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
# throws — guard errors degrade to allow.
function Get-RootUuid([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $null }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
} catch {}
return $null
}
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Get-EditMode([string]$cfgDir) {
try {
$pj = Find-V8Project (Get-Location).Path
if (-not $pj) { $pj = Find-V8Project $cfgDir }
if (-not $pj) { return 'deny' }
$proj = Get-Content -Raw $pj | ConvertFrom-Json
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
if ($proj.databases) {
foreach ($db in $proj.databases) {
if ($db.configSrc) {
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
}
}
}
}
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
return 'deny'
} catch { return 'deny' }
}
function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if (Test-ExternalObjectRoot $rp) { return }
$elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (Test-ExternalObjectRoot "$d.xml") { return }
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
}
if ($elemUuid -and $cfgDir) { break }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
# New object (no element file): fall back to config root uuid.
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
if (-not $binPath -or -not (Test-Path $binPath)) { return }
$bytes = [System.IO.File]::ReadAllBytes($binPath)
if ($bytes.Length -le 32) { return }
$start = 0
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
if (-not $hm.Success) { return }
$G = [int]$hm.Groups[1].Value
$K = [int]$hm.Groups[2].Value
if ($K -eq 0) { return }
$best = $null
if ($elemUuid) {
$u = [regex]::Escape($elemUuid.ToLower())
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
$f1 = [int]$m.Groups[1].Value
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
}
}
$blocked = $false; $code = ""; $reason = ""
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
elseif ($require -eq 'removed') {
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
}
else {
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
}
if (-not $blocked) { return }
$mode = Get-EditMode $cfgDir
if ($mode -eq 'off') { return }
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
# latter throws and would be swallowed by this function's own catch.
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if ($code -eq "capability-off") {
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
} elseif ($code -eq "not-removed") {
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
} else {
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
}
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
exit 1
} catch { return }
}
# --- Detect XML format version ---
# У корня <document> нет атрибута version, поэтому версию берём из конфигурации, в дерево
# которой пишем макет. Вне конфигурации (автономный .xml, исходники EPF) остаётся 2.17.
function Detect-FormatVersion([string]$dir) {
$d = $dir
while ($d) {
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
$extPath = "$d.xml"
if (Test-Path $extPath) {
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$cfgPath = Join-Path $d "Configuration.xml"
if (Test-Path $cfgPath) {
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$parent = Split-Path $d -Parent
if ($parent -eq $d) { break }
$d = $parent
}
return "2.17"
}
# Версия формата как число для сравнений: "2.20" → 220, "2.9" → 209.
# Строковое сравнение здесь неверно ("2.9" > "2.17" лексикографически) — известная ловушка.
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
$script:outPathResolved = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath }
$script:formatVersion = Detect-FormatVersion ([System.IO.Path]::GetDirectoryName($script:outPathResolved))
# --- 1. Load and validate JSON ---
if (-not (Test-Path $JsonPath)) {
Write-Error "File not found: $JsonPath"
exit 1
}
$json = Get-Content -Raw -Encoding UTF8 $JsonPath
$def = $json | ConvertFrom-Json
if (-not $def.columns) {
Write-Error "Required field 'columns' is missing"
exit 1
}
if (-not $def.areas) {
Write-Error "Required field 'areas' is missing"
exit 1
}
$totalColumns = [int]$def.columns
$defaultWidth = if ($def.defaultWidth) { [int]$def.defaultWidth } else { 10 }
# --- 2. Build font palette ---
$fontMap = [ordered]@{} # name -> 0-based index
$fontEntries = @() # array of hashtables
function Add-Font {
param([string]$name, $fontDef)
$face = if ($fontDef.face) { $fontDef.face } else { "Arial" }
$size = if ($fontDef.size) { [int]$fontDef.size } else { 10 }
$bold = if ($fontDef.bold -eq $true) { "true" } else { "false" }
$italic = if ($fontDef.italic -eq $true) { "true" } else { "false" }
$underline = if ($fontDef.underline -eq $true) { "true" } else { "false" }
$strikeout = if ($fontDef.strikeout -eq $true) { "true" } else { "false" }
$idx = $script:fontEntries.Count
$script:fontMap[$name] = $idx
$script:fontEntries += @{
Face = $face
Size = $size
Bold = $bold
Italic = $italic
Underline = $underline
Strikeout = $strikeout
}
}
# Add user-defined fonts
$hasDefault = $false
if ($def.fonts) {
foreach ($prop in $def.fonts.PSObject.Properties) {
if ($prop.Name -eq "default") { $hasDefault = $true }
Add-Font -name $prop.Name -fontDef $prop.Value
}
}
# Ensure default font exists
if (-not $hasDefault) {
$defaultDef = New-Object PSObject -Property @{ face = "Arial"; size = 10 }
Add-Font -name "default" -fontDef $defaultDef
}
# --- 3. Determine line palette ---
$hasThinBorders = $false
$hasThickBorders = $false
# Scan styles for border usage
if ($def.styles) {
foreach ($prop in $def.styles.PSObject.Properties) {
$s = $prop.Value
if ($s.border -and $s.border -ne "none") {
if ($s.borderWidth -eq "thick") {
$hasThickBorders = $true
} else {
$hasThinBorders = $true
}
}
}
}
$thinLineIndex = -1
$thickLineIndex = -1
$lineCount = 0
if ($hasThinBorders) {
$thinLineIndex = $lineCount; $lineCount++
}
if ($hasThickBorders) {
$thickLineIndex = $lineCount; $lineCount++
}
# --- 4. Parse column width specs ---
function Parse-ColumnSpec {
param([string]$spec)
$cols = @()
foreach ($part in $spec -split ',') {
$part = $part.Trim()
if ($part -match '^(\d+)-(\d+)$') {
$from = [int]$Matches[1]
$to = [int]$Matches[2]
for ($i = $from; $i -le $to; $i++) { $cols += $i }
} else {
$cols += [int]$part
}
}
return $cols
}
# --- 4a. Auto-calculate defaultWidth from page format ---
$pageTargets = @{
"A4-landscape" = 780
"A4-portrait" = 540
}
if ($def.page) {
$pageName = "$($def.page)"
$targetWidth = $null
if ($pageName -match '^\d+$') {
$targetWidth = [int]$pageName
} elseif ($pageTargets.ContainsKey($pageName)) {
$targetWidth = $pageTargets[$pageName]
} else {
Write-Warning "Unknown page format '$pageName'. Known: $($pageTargets.Keys -join ', '), or a number."
}
if ($targetWidth) {
$totalUnits = 0.0
$absoluteSum = 0
$specifiedCols = @{}
if ($def.columnWidths) {
foreach ($prop in $def.columnWidths.PSObject.Properties) {
$val = "$($prop.Value)"
$cols = Parse-ColumnSpec $prop.Name
foreach ($c in $cols) {
$specifiedCols[[int]$c] = $true
if ($val -match '^([0-9.]+)x$') {
$totalUnits += [double]$Matches[1]
} else {
$absoluteSum += [int]$val
}
}
}
}
for ($c = 1; $c -le $totalColumns; $c++) {
if (-not $specifiedCols.ContainsKey($c)) {
$totalUnits += 1.0
}
}
if ($totalUnits -gt 0) {
$defaultWidth = [int][math]::Round(($targetWidth - $absoluteSum) / $totalUnits)
}
}
}
# Build column width map: 1-based col -> width
$colWidthMap = @{}
if ($def.columnWidths) {
foreach ($prop in $def.columnWidths.PSObject.Properties) {
$val = "$($prop.Value)"
if ($val -match '^([0-9.]+)x$') {
$width = [int][math]::Round([double]$Matches[1] * $defaultWidth)
} else {
$width = [int]$val
}
$columns = Parse-ColumnSpec $prop.Name
foreach ($c in $columns) {
$colWidthMap[$c] = $width
}
}
}
# --- 5. Style resolver ---
function Resolve-Style {
param([string]$styleName, [string]$fillType)
$fontIdx = $fontMap["default"]
$lb = -1; $tb = -1; $rb = -1; $bb = -1
$ha = ""; $va = ""; $nf = ""
$wrap = $false
if ($styleName -and $def.styles) {
$style = $def.styles.$styleName
if ($style) {
# Font
if ($style.font -and $fontMap.Contains($style.font)) {
$fontIdx = $fontMap[$style.font]
}
# Borders
if ($style.border -and $style.border -ne "none") {
$lineIdx = if ($style.borderWidth -eq "thick") { $thickLineIndex } else { $thinLineIndex }
foreach ($side in ($style.border -split ',')) {
switch ($side.Trim()) {
"all" { $lb = $lineIdx; $tb = $lineIdx; $rb = $lineIdx; $bb = $lineIdx }
"left" { $lb = $lineIdx }
"top" { $tb = $lineIdx }
"right" { $rb = $lineIdx }
"bottom" { $bb = $lineIdx }
}
}
}
# Alignment
if ($style.align) {
switch ($style.align) {
"left" { $ha = "Left" }
"center" { $ha = "Center" }
"right" { $ha = "Right" }
}
}
if ($style.valign) {
switch ($style.valign) {
"top" { $va = "Top" }
"center" { $va = "Center" }
}
}
# Wrap
if ($style.wrap -eq $true) { $wrap = $true }
# Number format
if ($style.format) { $nf = $style.format }
}
}
return @{
FontIdx = $fontIdx
LB = $lb; TB = $tb; RB = $rb; BB = $bb
HA = $ha; VA = $va
Wrap = $wrap
FillType = $fillType
NumberFormat = $nf
}
}
# --- 6. Format palette builder ---
$formatRegistry = [ordered]@{} # key -> hashtable with properties
$formatOrder = @() # ordered keys for index assignment
function Get-FormatKey {
param(
[int]$fontIdx = -1,
[int]$lb = -1, [int]$tb = -1, [int]$rb = -1, [int]$bb = -1,
[string]$ha = "", [string]$va = "",
[bool]$wrap = $false,
[string]$fillType = "",
[string]$numberFormat = "",
[int]$width = -1,
[int]$height = -1
)
return "f=$fontIdx|lb=$lb|tb=$tb|rb=$rb|bb=$bb|ha=$ha|va=$va|wr=$wrap|ft=$fillType|nf=$numberFormat|w=$width|h=$height"
}
function Register-Format {
param([string]$key, [hashtable]$props)
if (-not $script:formatRegistry.Contains($key)) {
$script:formatRegistry[$key] = $props
$script:formatOrder += $key
}
# Return 1-based index
$idx = 0
foreach ($k in $script:formatRegistry.Keys) {
$idx++
if ($k -eq $key) { return $idx }
}
return $idx
}
# 6a. Default width format
$defaultFormatKey = Get-FormatKey -width $defaultWidth
$defaultFormatIndex = Register-Format -key $defaultFormatKey -props @{ Width = $defaultWidth }
# 6b. Column width formats
$colFormatMap = @{} # 1-based col -> format index
foreach ($col in ($colWidthMap.Keys | Sort-Object)) {
$w = $colWidthMap[$col]
$key = Get-FormatKey -width $w
$idx = Register-Format -key $key -props @{ Width = $w }
$colFormatMap[[int]$col] = $idx
}
# 6c. Scan areas for row heights and cell formats
# We need to do two passes: first collect all formats, then generate XML
# Helper: escape XML special characters
function Esc-Xml {
param([string]$s)
# Эскейп ЗНАЧЕНИЯ АТРИБУТА: & < > и кавычка — внутри "..." литеральная " невалидна.
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;').Replace('"','&quot;')
}
function Esc-XmlText {
# Экранирование ТЕКСТА элемента: только & < > . Кавычки в тексте платформа НЕ экранирует —
# пишет литерально (проверено: 92142 сырых кавычки на корпус, ни одной &quot;). &quot; платформа
# принимает, но при выгрузке нормализует обратно в кавычку → лишний шум в роундтрипе.
param([string]$s)
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;')
}
# Helper: determine fillType from cell content
function Get-FillType {
param($cell)
if ($cell.param) { return "Parameter" }
if ($cell.template) { return "Template" }
if ($cell.text) { return "Text" }
return ""
}
# Helper: register a cell format and return its index
function Register-CellFormat {
param($styleName, [string]$fillType)
$resolved = Resolve-Style -styleName $styleName -fillType $fillType
$key = Get-FormatKey -fontIdx $resolved.FontIdx `
-lb $resolved.LB -tb $resolved.TB -rb $resolved.RB -bb $resolved.BB `
-ha $resolved.HA -va $resolved.VA `
-wrap $resolved.Wrap -fillType $resolved.FillType `
-numberFormat $resolved.NumberFormat
$props = @{
FontIdx = $resolved.FontIdx
LB = $resolved.LB; TB = $resolved.TB
RB = $resolved.RB; BB = $resolved.BB
HA = $resolved.HA; VA = $resolved.VA
Wrap = $resolved.Wrap
FillType = $resolved.FillType
NumberFormat = $resolved.NumberFormat
}
return Register-Format -key $key -props $props
}
# Pre-register all formats from areas
foreach ($area in $def.areas) {
foreach ($row in $area.rows) {
# Skip empty row placeholder
if ($row.empty) { continue }
# Row height format
if ($row.height) {
$hKey = Get-FormatKey -height ([int]$row.height)
Register-Format -key $hKey -props @{ Height = [int]$row.height } | Out-Null
}
# rowStyle gap-fill format (no content → no fillType)
if ($row.rowStyle) {
Register-CellFormat -styleName $row.rowStyle -fillType "" | Out-Null
}
# Explicit cell formats
if ($row.cells) {
foreach ($cell in $row.cells) {
$cellStyle = if ($cell.style) { $cell.style } elseif ($row.rowStyle) { $row.rowStyle } else { "default" }
$ft = Get-FillType $cell
Register-CellFormat -styleName $cellStyle -fillType $ft | Out-Null
}
}
}
}
# --- 7. Generate XML ---
$xml = New-Object System.Text.StringBuilder 4096
function X {
param([string]$text)
$script:xml.AppendLine($text) | Out-Null
}
# 7a. Header
$docNsDecl = 'xmlns="http://v8.1c.ru/8.2/data/spreadsheet" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" 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"'
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (перед style):
# платформа держит объявления по алфавиту, дописать в конец нельзя.
if ((Get-FormatRank $script:formatVersion) -ge 221) {
$docNsDecl = $docNsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
}
X '<?xml version="1.0" encoding="UTF-8"?>'
X "<document $docNsDecl>"
# 7b. Language settings
X "`t<languageSettings>"
X "`t`t<currentLanguage>ru</currentLanguage>"
X "`t`t<defaultLanguage>ru</defaultLanguage>"
X "`t`t<languageInfo>"
X "`t`t`t<id>ru</id>"
X "`t`t`t<code>Русский</code>"
X "`t`t`t<description>Русский</description>"
X "`t`t</languageInfo>"
X "`t</languageSettings>"
# 7c. Columns
X "`t<columns>"
X "`t`t<size>$totalColumns</size>"
# Emit columnsItem for columns with non-default widths
foreach ($col in ($colFormatMap.Keys | Sort-Object)) {
$fmtIdx = $colFormatMap[$col]
$colIdx = $col - 1 # Convert to 0-based
X "`t`t<columnsItem>"
X "`t`t`t<index>$colIdx</index>"
X "`t`t`t<column>"
X "`t`t`t`t<formatIndex>$fmtIdx</formatIndex>"
X "`t`t`t</column>"
X "`t`t</columnsItem>"
}
X "`t</columns>"
# 7d. Rows — main generation loop
$globalRow = 0
$merges = @()
$namedItems = @()
$totalRowCount = 0
foreach ($area in $def.areas) {
$areaStartRow = $globalRow
$areaName = $area.name
$activeRowspans = @() # @{ColStart=1-based; ColEnd=1-based; EndLocalRow=int}
$localRow = 0
foreach ($row in $area.rows) {
# Empty row placeholder: emit N empty rows
if ($row.empty) {
$count = [int]$row.empty
for ($ei = 0; $ei -lt $count; $ei++) {
X "`t<rowsItem>"
X "`t`t<index>$globalRow</index>"
X "`t`t<row>"
X "`t`t`t<empty>true</empty>"
X "`t`t</row>"
X "`t</rowsItem>"
$globalRow++; $localRow++
}
continue
}
# Build set of columns occupied by rowspans from previous rows
$rowspanOccupied = @{} # 1-based col -> $true
foreach ($rs in $activeRowspans) {
if ($localRow -gt $rs.StartLocalRow -and $localRow -le $rs.EndLocalRow) {
for ($c = $rs.ColStart; $c -le $rs.ColEnd; $c++) {
$rowspanOccupied[$c] = $true
}
}
}
$rowHasContent = $false
$rowCells = @() # array of { Col(0-based), FormatIdx, Content }
# Determine row height format
$rowFormatIdx = 0
if ($row.height) {
$hKey = Get-FormatKey -height ([int]$row.height)
# Find format index for this key
$rIdx = 0
foreach ($k in $formatRegistry.Keys) {
$rIdx++
if ($k -eq $hKey) { $rowFormatIdx = $rIdx; break }
}
}
if ($row.cells -and $row.cells.Count -gt 0) {
$rowHasContent = $true
# Build set of occupied columns (1-based): explicit cells + rowspan from above
$occupiedCols = @{}
foreach ($rsk in $rowspanOccupied.Keys) { $occupiedCols[$rsk] = $true }
foreach ($cell in $row.cells) {
$colStart = [int]$cell.col
$colSpan = if ($cell.span) { [int]$cell.span } else { 1 }
for ($c = $colStart; $c -lt ($colStart + $colSpan); $c++) {
$occupiedCols[$c] = $true
}
}
# Generate explicit cells
foreach ($cell in $row.cells) {
$colStart = [int]$cell.col
$colSpan = if ($cell.span) { [int]$cell.span } else { 1 }
$rowspan = if ($cell.rowspan) { [int]$cell.rowspan } else { 1 }
$cellStyle = if ($cell.style) { $cell.style } elseif ($row.rowStyle) { $row.rowStyle } else { "default" }
$ft = Get-FillType $cell
$fmtIdx = Register-CellFormat -styleName $cellStyle -fillType $ft
$cellInfo = @{
Col = $colStart - 1 # 0-based
FormatIdx = $fmtIdx
Param = $cell.param
Detail = $cell.detail
Text = $cell.text
Template = $cell.template
}
$rowCells += $cellInfo
# Track rowspan for subsequent rows
if ($rowspan -gt 1) {
$activeRowspans += @{
ColStart = $colStart
ColEnd = $colStart + $colSpan - 1
StartLocalRow = $localRow
EndLocalRow = $localRow + $rowspan - 1
}
}
# Collect merge (horizontal, vertical, or both)
if ($colSpan -gt 1 -or $rowspan -gt 1) {
$merge = @{ R = $globalRow; C = $colStart - 1; W = $colSpan - 1 }
if ($rowspan -gt 1) { $merge.H = $rowspan - 1 }
$merges += $merge
}
}
# Generate gap-fill cells for rowStyle
if ($row.rowStyle) {
$gapFmtIdx = Register-CellFormat -styleName $row.rowStyle -fillType ""
for ($c = 1; $c -le $totalColumns; $c++) {
if (-not $occupiedCols.ContainsKey($c)) {
$rowCells += @{
Col = $c - 1 # 0-based
FormatIdx = $gapFmtIdx
Param = $null
Detail = $null
Text = $null
Template = $null
}
}
}
}
# Sort cells by column
$rowCells = $rowCells | Sort-Object { $_.Col }
} elseif ($row.rowStyle) {
# Row with only rowStyle, no explicit cells — fill non-rowspan columns
$rowHasContent = $true
$gapFmtIdx = Register-CellFormat -styleName $row.rowStyle -fillType ""
for ($c = 1; $c -le $totalColumns; $c++) {
if ($rowspanOccupied.ContainsKey($c)) { continue }
$rowCells += @{
Col = $c - 1
FormatIdx = $gapFmtIdx
Param = $null
Detail = $null
Text = $null
Template = $null
}
}
}
# Emit rowsItem
X "`t<rowsItem>"
X "`t`t<index>$globalRow</index>"
X "`t`t<row>"
if ($rowFormatIdx -gt 0) {
X "`t`t`t<formatIndex>$rowFormatIdx</formatIndex>"
}
if (-not $rowHasContent) {
X "`t`t`t<empty>true</empty>"
} else {
foreach ($cellInfo in $rowCells) {
X "`t`t`t<c>"
X "`t`t`t`t<i>$($cellInfo.Col)</i>"
X "`t`t`t`t<c>"
X "`t`t`t`t`t<f>$($cellInfo.FormatIdx)</f>"
if ($cellInfo.Param) {
X "`t`t`t`t`t<parameter>$($cellInfo.Param)</parameter>"
if ($cellInfo.Detail) {
X "`t`t`t`t`t<detailParameter>$($cellInfo.Detail)</detailParameter>"
}
}
if ($cellInfo.Text) {
X "`t`t`t`t`t<tl>"
X "`t`t`t`t`t`t<v8:item>"
X "`t`t`t`t`t`t`t<v8:lang>ru</v8:lang>"
X "`t`t`t`t`t`t`t<v8:content>$(Esc-XmlText $cellInfo.Text)</v8:content>"
X "`t`t`t`t`t`t</v8:item>"
X "`t`t`t`t`t</tl>"
}
if ($cellInfo.Template) {
X "`t`t`t`t`t<tl>"
X "`t`t`t`t`t`t<v8:item>"
X "`t`t`t`t`t`t`t<v8:lang>ru</v8:lang>"
X "`t`t`t`t`t`t`t<v8:content>$(Esc-XmlText $cellInfo.Template)</v8:content>"
X "`t`t`t`t`t`t</v8:item>"
X "`t`t`t`t`t</tl>"
}
X "`t`t`t`t</c>"
X "`t`t`t</c>"
}
}
X "`t`t</row>"
X "`t</rowsItem>"
$localRow++
$globalRow++
}
$areaEndRow = $globalRow - 1
$namedItems += @{
Name = $areaName
BeginRow = $areaStartRow
EndRow = $areaEndRow
}
}
$totalRowCount = $globalRow
# 7e. Scalar metadata
X "`t<templateMode>true</templateMode>"
X "`t<defaultFormatIndex>$defaultFormatIndex</defaultFormatIndex>"
X "`t<height>$totalRowCount</height>"
X "`t<vgRows>$totalRowCount</vgRows>"
# 7f. Merges
foreach ($m in $merges) {
X "`t<merge>"
X "`t`t<r>$($m.R)</r>"
X "`t`t<c>$($m.C)</c>"
if ($m.H) { X "`t`t<h>$($m.H)</h>" }
X "`t`t<w>$($m.W)</w>"
X "`t</merge>"
}
# 7g. Named items
foreach ($ni in $namedItems) {
X "`t<namedItem xsi:type=`"NamedItemCells`">"
X "`t`t<name>$($ni.Name)</name>"
X "`t`t<area>"
X "`t`t`t<type>Rows</type>"
X "`t`t`t<beginRow>$($ni.BeginRow)</beginRow>"
X "`t`t`t<endRow>$($ni.EndRow)</endRow>"
X "`t`t`t<beginColumn>-1</beginColumn>"
X "`t`t`t<endColumn>-1</endColumn>"
X "`t`t</area>"
X "`t</namedItem>"
}
# 7h. Line palette
if ($hasThinBorders) {
X "`t<line width=`"1`" gap=`"false`">"
X "`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">Solid</v8ui:style>"
X "`t</line>"
}
if ($hasThickBorders) {
X "`t<line width=`"2`" gap=`"false`">"
X "`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">Solid</v8ui:style>"
X "`t</line>"
}
# 7i. Font palette
foreach ($fe in $fontEntries) {
X "`t<font faceName=`"$($fe.Face)`" height=`"$($fe.Size)`" bold=`"$($fe.Bold)`" italic=`"$($fe.Italic)`" underline=`"$($fe.Underline)`" strikeout=`"$($fe.Strikeout)`" kind=`"Absolute`" scale=`"100`"/>"
}
# 7j. Format palette
foreach ($key in $formatRegistry.Keys) {
$fmt = $formatRegistry[$key]
X "`t<format>"
if ($fmt.FontIdx -ne $null -and $fmt.FontIdx -ge 0) {
X "`t`t<font>$($fmt.FontIdx)</font>"
}
if ($fmt.LB -ne $null -and $fmt.LB -ge 0) {
X "`t`t<leftBorder>$($fmt.LB)</leftBorder>"
}
if ($fmt.TB -ne $null -and $fmt.TB -ge 0) {
X "`t`t<topBorder>$($fmt.TB)</topBorder>"
}
if ($fmt.RB -ne $null -and $fmt.RB -ge 0) {
X "`t`t<rightBorder>$($fmt.RB)</rightBorder>"
}
if ($fmt.BB -ne $null -and $fmt.BB -ge 0) {
X "`t`t<bottomBorder>$($fmt.BB)</bottomBorder>"
}
if ($fmt.Width) {
X "`t`t<width>$($fmt.Width)</width>"
}
if ($fmt.Height) {
X "`t`t<height>$($fmt.Height)</height>"
}
if ($fmt.HA) {
X "`t`t<horizontalAlignment>$($fmt.HA)</horizontalAlignment>"
}
if ($fmt.VA) {
X "`t`t<verticalAlignment>$($fmt.VA)</verticalAlignment>"
}
if ($fmt.Wrap -eq $true) {
X "`t`t<textPlacement>Wrap</textPlacement>"
}
if ($fmt.FillType) {
X "`t`t<fillType>$($fmt.FillType)</fillType>"
}
if ($fmt.NumberFormat) {
X "`t`t<format>"
X "`t`t`t<v8:item>"
X "`t`t`t`t<v8:lang>ru</v8:lang>"
X "`t`t`t`t<v8:content>$(Esc-XmlText $fmt.NumberFormat)</v8:content>"
X "`t`t`t</v8:item>"
X "`t`t</format>"
}
X "`t</format>"
}
# 7k. Close document
X '</document>'
# --- 8. Write output ---
$enc = New-Object System.Text.UTF8Encoding($true)
$resolvedPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath }
# Каталог назначения создаём сами: типовой путь — Templates/<Имя>/Ext/Template.xml,
# и его может ещё не быть. Так делают и form-compile, и skd-compile, и py-порт этого
# навыка; без этого PS-порт падал на «Could not find a part of the path».
$outDir = [System.IO.Path]::GetDirectoryName($resolvedPath)
if ($outDir -and -not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir -Force | Out-Null }
Assert-EditAllowed $resolvedPath 'editable'
[System.IO.File]::WriteAllText($resolvedPath, $xml.ToString().TrimEnd("`r", "`n"), $enc)
# --- 9. Summary ---
Write-Host "[OK] Compiled: $OutputPath"
if ($def.page) {
Write-Host " Page: $pageName -> target $targetWidth, defaultWidth=$defaultWidth"
}
Write-Host " Areas: $($namedItems.Count), Rows: $totalRowCount, Columns: $totalColumns"
Write-Host " Fonts: $($fontEntries.Count), Lines: $lineCount, Formats: $($formatRegistry.Count)"
Write-Host " Merges: $($merges.Count)"
@@ -1,932 +0,0 @@
#!/usr/bin/env python3
# mxl-compile v1.14 — Compile 1C spreadsheet from JSON (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import math
import os
import re
import sys
from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
class CIDict(dict):
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
# сохраняется — от него зависит порядок эмиссии.
def _actual(self, key):
if not isinstance(key, str) or dict.__contains__(self, key):
return key
ci = self.__dict__.get('_ci')
if ci is None or len(ci) != len(self):
ci = {k.lower(): k for k in self if isinstance(k, str)}
self.__dict__['_ci'] = ci
return ci.get(key.lower(), key)
def __getitem__(self, key):
return dict.__getitem__(self, self._actual(key))
def __contains__(self, key):
return dict.__contains__(self, self._actual(key))
def get(self, key, default=None):
return dict.get(self, self._actual(key), default)
def pop(self, key, *default):
return dict.pop(self, self._actual(key), *default)
def __setitem__(self, key, value):
# запись по ключу, отличающемуся регистром, обновляет существующий, а не плодит дубль
dict.__setitem__(self, self._actual(key), value)
def ci_json(obj):
"""Рекурсивно оборачивает разобранный JSON: словари → CIDict, списки обходятся."""
if isinstance(obj, dict):
return CIDict((k, ci_json(v)) for k, v in obj.items())
if isinstance(obj, list):
return [ci_json(v) for v in obj]
return obj
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
# ============================================================
def _sg_root_uuid(xml_path):
if not os.path.isfile(xml_path):
return None
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str) and child.get("uuid"):
return child.get("uuid")
except Exception:
return None
return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
if not d:
break
pj = os.path.join(d, ".v8-project.json")
if os.path.isfile(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def _sg_get_edit_mode(cfg_dir):
try:
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
if not pj:
return "deny"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src:
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
if db.get("editingAllowedCheck"):
return db["editingAllowedCheck"]
if proj.get("editingAllowedCheck"):
return proj["editingAllowedCheck"]
return "deny"
except Exception:
return "deny"
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if _sg_is_external_root(rp):
return
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
for _ in range(12):
if not d:
break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
cfg_dir = d
bin_path = cand
if elem_uuid and cfg_dir:
break
parent = os.path.dirname(d)
if parent == d:
break
d = parent
if not elem_uuid and cfg_dir:
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
if not bin_path or not os.path.exists(bin_path):
return
data = open(bin_path, "rb").read()
if len(data) <= 32:
return
if data[:3] == b"\xef\xbb\xbf":
data = data[3:]
text = data.decode("utf-8", "replace")
h = re.match(r"\{6,(\d+),(\d+),", text)
if not h:
return
g = int(h.group(1))
k = int(h.group(2))
if k == 0:
return
best = None
if elem_uuid:
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
f1 = int(m.group(1))
if best is None or f1 < best:
best = f1
blocked = False
code = ""
reason = ""
if g == 1:
blocked = True
code = "capability-off"
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
elif require == "removed":
if best is not None and best != 2:
blocked = True
code = "not-removed"
reason = "объект не снят с поддержки — удаление сломает обновления"
else:
if best is not None and best == 0:
blocked = True
code = "locked"
reason = "объект на замке — редактирование сломает обновления"
if not blocked:
return
mode = _sg_get_edit_mode(cfg_dir)
if mode == "off":
return
if mode == "warn":
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
return
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if code == "capability-off":
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
fix = (
"Либо снять защиту явно (навык support-edit, два шага):\n"
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
)
elif code == "not-removed":
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
fix = (
"Либо сначала снять объект с поддержки, затем удалять:\n"
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
)
else:
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
fix = (
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
)
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
sys.exit(1)
except SystemExit:
raise
except Exception:
return
def esc_xml(s):
# Эскейп ЗНАЧЕНИЯ АТРИБУТА: & < > и кавычка — внутри "..." литеральная " невалидна.
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
def esc_xml_text(s):
"""Экранирование ТЕКСТА элемента: только & < > . Кавычки платформа в тексте не экранирует
(92142 сырых кавычки на корпус, ни одной &quot;); &quot; она принимает, но нормализует обратно."""
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
def write_utf8_bom(path, content):
# newline='' — без трансляции: иначе текстовый режим Python дал бы CRLF на Windows
# и LF на macOS, то есть вывод навыка зависел бы от ОС.
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def detect_format_version(d):
while d:
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
ext_path = d + ".xml"
if os.path.isfile(ext_path):
with open(ext_path, "r", encoding="utf-8-sig") as f:
ext_head = f.read(2000)
if re.search(r'<(ExternalDataProcessor|ExternalReport)[ >]', ext_head):
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', ext_head)
if m:
return m.group(1)
cfg_path = os.path.join(d, "Configuration.xml")
if os.path.isfile(cfg_path):
with open(cfg_path, "r", encoding="utf-8-sig") as f:
head = f.read(2000)
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', head)
if m:
return m.group(1)
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return "2.17"
def format_rank(ver):
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description='Compile 1C spreadsheet from JSON', allow_abbrev=False)
parser.add_argument('-JsonPath', type=str, required=True)
parser.add_argument('-OutputPath', type=str, required=True)
args = ci_parse_args(parser)
# --- Detect XML format version ---
# У корня <document> нет атрибута version, поэтому версию берём из конфигурации, в дерево
# которой пишем макет. Вне конфигурации (автономный .xml, исходники EPF) остаётся 2.17.
out_path_resolved = args.OutputPath if os.path.isabs(args.OutputPath) else os.path.join(os.getcwd(), args.OutputPath)
format_version = detect_format_version(os.path.dirname(out_path_resolved))
# --- 1. Load and validate JSON ---
json_path = args.JsonPath
if not os.path.exists(json_path):
print(f"File not found: {json_path}", file=sys.stderr)
sys.exit(1)
with open(json_path, 'r', encoding='utf-8-sig') as f:
defn = ci_json(json.load(f))
if not defn.get('columns'):
print("Required field 'columns' is missing", file=sys.stderr)
sys.exit(1)
if not defn.get('areas'):
print("Required field 'areas' is missing", file=sys.stderr)
sys.exit(1)
total_columns = int(defn['columns'])
default_width = int(defn['defaultWidth']) if defn.get('defaultWidth') else 10
# --- 2. Build font palette ---
font_map = {} # name -> 0-based index
font_entries = [] # list of dicts
def add_font(name, font_def):
face = font_def.get('face', 'Arial') if font_def else 'Arial'
size = int(font_def.get('size', 10)) if font_def else 10
bold = 'true' if font_def and font_def.get('bold') is True else 'false'
italic = 'true' if font_def and font_def.get('italic') is True else 'false'
underline = 'true' if font_def and font_def.get('underline') is True else 'false'
strikeout = 'true' if font_def and font_def.get('strikeout') is True else 'false'
idx = len(font_entries)
font_map[name] = idx
font_entries.append({
'Face': face,
'Size': size,
'Bold': bold,
'Italic': italic,
'Underline': underline,
'Strikeout': strikeout,
})
# Add user-defined fonts
has_default = False
if defn.get('fonts'):
for fname, fdef in defn['fonts'].items():
if fname == 'default':
has_default = True
add_font(fname, fdef)
# Ensure default font exists
if not has_default:
add_font('default', {'face': 'Arial', 'size': 10})
# --- 3. Determine line palette ---
has_thin_borders = False
has_thick_borders = False
if defn.get('styles'):
for sname, sval in defn['styles'].items():
if sval.get('border') and sval['border'] != 'none':
if sval.get('borderWidth') == 'thick':
has_thick_borders = True
else:
has_thin_borders = True
thin_line_index = -1
thick_line_index = -1
line_count = 0
if has_thin_borders:
thin_line_index = line_count
line_count += 1
if has_thick_borders:
thick_line_index = line_count
line_count += 1
# --- 4. Parse column width specs ---
def parse_column_spec(spec):
cols = []
for part in spec.split(','):
part = part.strip()
m = re.match(r'^(\d+)-(\d+)$', part)
if m:
from_col = int(m.group(1))
to_col = int(m.group(2))
for i in range(from_col, to_col + 1):
cols.append(i)
else:
cols.append(int(part))
return cols
# --- 4a. Auto-calculate defaultWidth from page format ---
page_targets = {
'A4-landscape': 780,
'A4-portrait': 540,
}
page_name = None
target_width = None
if defn.get('page'):
page_name = str(defn['page'])
if re.match(r'^\d+$', page_name):
target_width = int(page_name)
elif page_name in page_targets:
target_width = page_targets[page_name]
else:
print(f"WARNING: Unknown page format '{page_name}'. Known: {', '.join(page_targets.keys())}, or a number.", file=sys.stderr)
if target_width:
total_units = 0.0
absolute_sum = 0
specified_cols = {}
if defn.get('columnWidths'):
for prop_name, prop_value in defn['columnWidths'].items():
val = str(prop_value)
cols = parse_column_spec(prop_name)
for c in cols:
specified_cols[int(c)] = True
m = re.match(r'^([0-9.]+)x$', val)
if m:
total_units += float(m.group(1))
else:
absolute_sum += int(val)
for c in range(1, total_columns + 1):
if c not in specified_cols:
total_units += 1.0
if total_units > 0:
default_width = round((target_width - absolute_sum) / total_units)
# Build column width map: 1-based col -> width
col_width_map = {}
if defn.get('columnWidths'):
for prop_name, prop_value in defn['columnWidths'].items():
val = str(prop_value)
m = re.match(r'^([0-9.]+)x$', val)
if m:
width = round(float(m.group(1)) * default_width)
else:
width = int(val)
columns = parse_column_spec(prop_name)
for c in columns:
col_width_map[c] = width
# --- 5. Style resolver ---
def resolve_style(style_name, fill_type):
font_idx = font_map.get('default', 0)
lb = -1; tb = -1; rb = -1; bb = -1
ha = ''; va = ''; nf = ''
wrap = False
if style_name and defn.get('styles'):
style = defn['styles'].get(style_name)
if style:
# Font
if style.get('font') and style['font'] in font_map:
font_idx = font_map[style['font']]
# Borders
if style.get('border') and style['border'] != 'none':
line_idx = thick_line_index if style.get('borderWidth') == 'thick' else thin_line_index
for side in style['border'].split(','):
side = side.strip()
if side == 'all':
lb = line_idx; tb = line_idx; rb = line_idx; bb = line_idx
elif side == 'left':
lb = line_idx
elif side == 'top':
tb = line_idx
elif side == 'right':
rb = line_idx
elif side == 'bottom':
bb = line_idx
# Alignment
if style.get('align'):
align_map = {'left': 'Left', 'center': 'Center', 'right': 'Right'}
ha = align_map.get(style['align'], '')
if style.get('valign'):
valign_map = {'top': 'Top', 'center': 'Center'}
va = valign_map.get(style['valign'], '')
# Wrap
if style.get('wrap') is True:
wrap = True
# Number format
if style.get('format'):
nf = style['format']
return {
'FontIdx': font_idx,
'LB': lb, 'TB': tb, 'RB': rb, 'BB': bb,
'HA': ha, 'VA': va,
'Wrap': wrap,
'FillType': fill_type,
'NumberFormat': nf,
}
# --- 6. Format palette builder ---
format_registry = {} # key -> props
format_order = [] # ordered keys for index assignment
def get_format_key(font_idx=-1, lb=-1, tb=-1, rb=-1, bb=-1, ha='', va='',
wrap=False, fill_type='', number_format='', width=-1, height=-1):
return f'f={font_idx}|lb={lb}|tb={tb}|rb={rb}|bb={bb}|ha={ha}|va={va}|wr={wrap}|ft={fill_type}|nf={number_format}|w={width}|h={height}'
def register_format(key, props):
if key not in format_registry:
format_registry[key] = props
format_order.append(key)
# Return 1-based index
return format_order.index(key) + 1
# 6a. Default width format
default_format_key = get_format_key(width=default_width)
default_format_index = register_format(default_format_key, {'Width': default_width})
# 6b. Column width formats
col_format_map = {} # 1-based col -> format index
for col in sorted(col_width_map):
w = col_width_map[col]
key = get_format_key(width=w)
idx = register_format(key, {'Width': w})
col_format_map[int(col)] = idx
# 6c. Helper: determine fillType from cell content
def get_fill_type(cell):
if cell.get('param'):
return 'Parameter'
if cell.get('template'):
return 'Template'
if cell.get('text'):
return 'Text'
return ''
# Helper: register a cell format and return its index
def register_cell_format(style_name, fill_type):
resolved = resolve_style(style_name, fill_type)
key = get_format_key(
font_idx=resolved['FontIdx'],
lb=resolved['LB'], tb=resolved['TB'], rb=resolved['RB'], bb=resolved['BB'],
ha=resolved['HA'], va=resolved['VA'],
wrap=resolved['Wrap'], fill_type=resolved['FillType'],
number_format=resolved['NumberFormat'])
props = {
'FontIdx': resolved['FontIdx'],
'LB': resolved['LB'], 'TB': resolved['TB'],
'RB': resolved['RB'], 'BB': resolved['BB'],
'HA': resolved['HA'], 'VA': resolved['VA'],
'Wrap': resolved['Wrap'],
'FillType': resolved['FillType'],
'NumberFormat': resolved['NumberFormat'],
}
return register_format(key, props)
# Pre-register all formats from areas
for area in defn['areas']:
for row in area.get('rows', []):
# Skip list-of-values shorthand rows (treated as empty rows like PS1)
if isinstance(row, list):
continue
# Skip empty row placeholder
if row.get('empty'):
continue
# Row height format
if row.get('height'):
h_key = get_format_key(height=int(row['height']))
register_format(h_key, {'Height': int(row['height'])})
# rowStyle gap-fill format
if row.get('rowStyle'):
register_cell_format(row['rowStyle'], '')
# Explicit cell formats
if row.get('cells'):
for cell in row['cells']:
cell_style = cell.get('style') or row.get('rowStyle') or 'default'
ft = get_fill_type(cell)
register_cell_format(cell_style, ft)
# --- 7. Generate XML ---
lines = []
# 7a. Header
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
doc_ns_decl = ('xmlns="http://v8.1c.ru/8.2/data/spreadsheet" xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
' 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"')
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (перед style):
# платформа держит объявления по алфавиту, дописать в конец нельзя.
if format_rank(format_version) >= 221:
doc_ns_decl = doc_ns_decl.replace(
' xmlns:style=',
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
lines.append(f'<document {doc_ns_decl}>')
# 7b. Language settings
lines.append('\t<languageSettings>')
lines.append('\t\t<currentLanguage>ru</currentLanguage>')
lines.append('\t\t<defaultLanguage>ru</defaultLanguage>')
lines.append('\t\t<languageInfo>')
lines.append('\t\t\t<id>ru</id>')
lines.append('\t\t\t<code>\u0420\u0443\u0441\u0441\u043a\u0438\u0439</code>')
lines.append('\t\t\t<description>\u0420\u0443\u0441\u0441\u043a\u0438\u0439</description>')
lines.append('\t\t</languageInfo>')
lines.append('\t</languageSettings>')
# 7c. Columns
lines.append('\t<columns>')
lines.append(f'\t\t<size>{total_columns}</size>')
# Emit columnsItem for columns with non-default widths
for col in sorted(col_format_map.keys()):
fmt_idx = col_format_map[col]
col_idx = col - 1 # Convert to 0-based
lines.append('\t\t<columnsItem>')
lines.append(f'\t\t\t<index>{col_idx}</index>')
lines.append('\t\t\t<column>')
lines.append(f'\t\t\t\t<formatIndex>{fmt_idx}</formatIndex>')
lines.append('\t\t\t</column>')
lines.append('\t\t</columnsItem>')
lines.append('\t</columns>')
# 7d. Rows -- main generation loop
global_row = 0
merges = []
named_items = []
active_rowspans = [] # list of {ColStart, ColEnd, StartLocalRow, EndLocalRow}
for area in defn['areas']:
area_start_row = global_row
area_name = area.get('name', '')
active_rowspans = []
local_row = 0
for row in area.get('rows', []):
# List-of-values shorthand: treat as row with no properties (like PS1)
if isinstance(row, list):
row = {}
# Empty row placeholder: emit N empty rows
if row.get('empty'):
count = int(row['empty'])
for ei in range(count):
lines.append('\t<rowsItem>')
lines.append(f'\t\t<index>{global_row}</index>')
lines.append('\t\t<row>')
lines.append('\t\t\t<empty>true</empty>')
lines.append('\t\t</row>')
lines.append('\t</rowsItem>')
global_row += 1
local_row += 1
continue
# Build set of columns occupied by rowspans from previous rows
rowspan_occupied = {}
for rs in active_rowspans:
if local_row > rs['StartLocalRow'] and local_row <= rs['EndLocalRow']:
for c in range(rs['ColStart'], rs['ColEnd'] + 1):
rowspan_occupied[c] = True
row_has_content = False
row_cells = []
# Determine row height format
row_format_idx = 0
if row.get('height'):
h_key = get_format_key(height=int(row['height']))
if h_key in format_registry:
row_format_idx = format_order.index(h_key) + 1
if row.get('cells') and len(row['cells']) > 0:
row_has_content = True
# Build set of occupied columns (1-based)
occupied_cols = dict(rowspan_occupied)
for cell in row['cells']:
col_start = int(cell['col'])
col_span = int(cell.get('span', 1))
for c in range(col_start, col_start + col_span):
occupied_cols[c] = True
# Generate explicit cells
for cell in row['cells']:
col_start = int(cell['col'])
col_span = int(cell.get('span', 1))
rowspan = int(cell.get('rowspan', 1))
cell_style = cell.get('style') or row.get('rowStyle') or 'default'
ft = get_fill_type(cell)
fmt_idx = register_cell_format(cell_style, ft)
cell_info = {
'Col': col_start - 1, # 0-based
'FormatIdx': fmt_idx,
'Param': cell.get('param'),
'Detail': cell.get('detail'),
'Text': cell.get('text'),
'Template': cell.get('template'),
}
row_cells.append(cell_info)
# Track rowspan for subsequent rows
if rowspan > 1:
active_rowspans.append({
'ColStart': col_start,
'ColEnd': col_start + col_span - 1,
'StartLocalRow': local_row,
'EndLocalRow': local_row + rowspan - 1,
})
# Collect merge
if col_span > 1 or rowspan > 1:
merge = {'R': global_row, 'C': col_start - 1, 'W': col_span - 1}
if rowspan > 1:
merge['H'] = rowspan - 1
merges.append(merge)
# Generate gap-fill cells for rowStyle
if row.get('rowStyle'):
gap_fmt_idx = register_cell_format(row['rowStyle'], '')
for c in range(1, total_columns + 1):
if c not in occupied_cols:
row_cells.append({
'Col': c - 1,
'FormatIdx': gap_fmt_idx,
'Param': None,
'Detail': None,
'Text': None,
'Template': None,
})
# Sort cells by column
row_cells.sort(key=lambda x: x['Col'])
elif row.get('rowStyle'):
# Row with only rowStyle, no explicit cells
row_has_content = True
gap_fmt_idx = register_cell_format(row['rowStyle'], '')
for c in range(1, total_columns + 1):
if c in rowspan_occupied:
continue
row_cells.append({
'Col': c - 1,
'FormatIdx': gap_fmt_idx,
'Param': None,
'Detail': None,
'Text': None,
'Template': None,
})
# Emit rowsItem
lines.append('\t<rowsItem>')
lines.append(f'\t\t<index>{global_row}</index>')
lines.append('\t\t<row>')
if row_format_idx > 0:
lines.append(f'\t\t\t<formatIndex>{row_format_idx}</formatIndex>')
if not row_has_content:
lines.append('\t\t\t<empty>true</empty>')
else:
for cell_info in row_cells:
lines.append('\t\t\t<c>')
lines.append(f'\t\t\t\t<i>{cell_info["Col"]}</i>')
lines.append('\t\t\t\t<c>')
lines.append(f'\t\t\t\t\t<f>{cell_info["FormatIdx"]}</f>')
if cell_info['Param']:
lines.append(f'\t\t\t\t\t<parameter>{cell_info["Param"]}</parameter>')
if cell_info['Detail']:
lines.append(f'\t\t\t\t\t<detailParameter>{cell_info["Detail"]}</detailParameter>')
if cell_info['Text']:
lines.append('\t\t\t\t\t<tl>')
lines.append('\t\t\t\t\t\t<v8:item>')
lines.append('\t\t\t\t\t\t\t<v8:lang>ru</v8:lang>')
lines.append(f'\t\t\t\t\t\t\t<v8:content>{esc_xml_text(cell_info["Text"])}</v8:content>')
lines.append('\t\t\t\t\t\t</v8:item>')
lines.append('\t\t\t\t\t</tl>')
if cell_info['Template']:
lines.append('\t\t\t\t\t<tl>')
lines.append('\t\t\t\t\t\t<v8:item>')
lines.append('\t\t\t\t\t\t\t<v8:lang>ru</v8:lang>')
lines.append(f'\t\t\t\t\t\t\t<v8:content>{esc_xml_text(cell_info["Template"])}</v8:content>')
lines.append('\t\t\t\t\t\t</v8:item>')
lines.append('\t\t\t\t\t</tl>')
lines.append('\t\t\t\t</c>')
lines.append('\t\t\t</c>')
lines.append('\t\t</row>')
lines.append('\t</rowsItem>')
local_row += 1
global_row += 1
area_end_row = global_row - 1
named_items.append({
'Name': area_name,
'BeginRow': area_start_row,
'EndRow': area_end_row,
})
total_row_count = global_row
# 7e. Scalar metadata
lines.append(f'\t<templateMode>true</templateMode>')
lines.append(f'\t<defaultFormatIndex>{default_format_index}</defaultFormatIndex>')
lines.append(f'\t<height>{total_row_count}</height>')
lines.append(f'\t<vgRows>{total_row_count}</vgRows>')
# 7f. Merges
for m in merges:
lines.append('\t<merge>')
lines.append(f'\t\t<r>{m["R"]}</r>')
lines.append(f'\t\t<c>{m["C"]}</c>')
if m.get('H'):
lines.append(f'\t\t<h>{m["H"]}</h>')
lines.append(f'\t\t<w>{m["W"]}</w>')
lines.append('\t</merge>')
# 7g. Named items
for ni in named_items:
lines.append('\t<namedItem xsi:type="NamedItemCells">')
lines.append(f'\t\t<name>{ni["Name"]}</name>')
lines.append('\t\t<area>')
lines.append('\t\t\t<type>Rows</type>')
lines.append(f'\t\t\t<beginRow>{ni["BeginRow"]}</beginRow>')
lines.append(f'\t\t\t<endRow>{ni["EndRow"]}</endRow>')
lines.append('\t\t\t<beginColumn>-1</beginColumn>')
lines.append('\t\t\t<endColumn>-1</endColumn>')
lines.append('\t\t</area>')
lines.append('\t</namedItem>')
# 7h. Line palette
if has_thin_borders:
lines.append('\t<line width="1" gap="false">')
lines.append('\t\t<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">Solid</v8ui:style>')
lines.append('\t</line>')
if has_thick_borders:
lines.append('\t<line width="2" gap="false">')
lines.append('\t\t<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">Solid</v8ui:style>')
lines.append('\t</line>')
# 7i. Font palette
for fe in font_entries:
lines.append(f'\t<font faceName="{fe["Face"]}" height="{fe["Size"]}" bold="{fe["Bold"]}" italic="{fe["Italic"]}" underline="{fe["Underline"]}" strikeout="{fe["Strikeout"]}" kind="Absolute" scale="100"/>')
# 7j. Format palette
for key in format_order:
fmt = format_registry[key]
lines.append('\t<format>')
if fmt.get('FontIdx') is not None and fmt.get('FontIdx', -1) >= 0:
lines.append(f'\t\t<font>{fmt["FontIdx"]}</font>')
if fmt.get('LB') is not None and fmt.get('LB', -1) >= 0:
lines.append(f'\t\t<leftBorder>{fmt["LB"]}</leftBorder>')
if fmt.get('TB') is not None and fmt.get('TB', -1) >= 0:
lines.append(f'\t\t<topBorder>{fmt["TB"]}</topBorder>')
if fmt.get('RB') is not None and fmt.get('RB', -1) >= 0:
lines.append(f'\t\t<rightBorder>{fmt["RB"]}</rightBorder>')
if fmt.get('BB') is not None and fmt.get('BB', -1) >= 0:
lines.append(f'\t\t<bottomBorder>{fmt["BB"]}</bottomBorder>')
if fmt.get('Width'):
lines.append(f'\t\t<width>{fmt["Width"]}</width>')
if fmt.get('Height'):
lines.append(f'\t\t<height>{fmt["Height"]}</height>')
if fmt.get('HA'):
lines.append(f'\t\t<horizontalAlignment>{fmt["HA"]}</horizontalAlignment>')
if fmt.get('VA'):
lines.append(f'\t\t<verticalAlignment>{fmt["VA"]}</verticalAlignment>')
if fmt.get('Wrap') is True:
lines.append('\t\t<textPlacement>Wrap</textPlacement>')
if fmt.get('FillType'):
lines.append(f'\t\t<fillType>{fmt["FillType"]}</fillType>')
if fmt.get('NumberFormat'):
lines.append('\t\t<format>')
lines.append('\t\t\t<v8:item>')
lines.append('\t\t\t\t<v8:lang>ru</v8:lang>')
lines.append(f'\t\t\t\t<v8:content>{esc_xml_text(fmt["NumberFormat"])}</v8:content>')
lines.append('\t\t\t</v8:item>')
lines.append('\t\t</format>')
lines.append('\t</format>')
# 7k. Close document
lines.append('</document>')
# --- 8. Write output ---
out_path = args.OutputPath
if not os.path.isabs(out_path):
out_path = os.path.join(os.getcwd(), out_path)
assert_edit_allowed(out_path, "editable")
out_dir = os.path.dirname(out_path)
if out_dir and not os.path.exists(out_dir):
os.makedirs(out_dir, exist_ok=True)
content = '\r\n'.join(lines)
write_utf8_bom(out_path, content)
# --- 9. Summary ---
print(f"[OK] Compiled: {args.OutputPath}")
if defn.get('page'):
print(f" Page: {page_name} -> target {target_width}, defaultWidth={default_width}")
print(f" Areas: {len(named_items)}, Rows: {total_row_count}, Columns: {total_columns}")
print(f" Fonts: {len(font_entries)}, Lines: {line_count}, Formats: {len(format_registry)}")
print(f" Merges: {len(merges)}")
if __name__ == '__main__':
main()
@@ -1,646 +0,0 @@
# mxl-decompile v1.1 — Decompile 1C spreadsheet to JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[Alias('Path')]
[string]$TemplatePath,
[string]$OutputPath
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- 1. Load and parse XML ---
if (-not (Test-Path $TemplatePath)) {
Write-Error "File not found: $TemplatePath"
exit 1
}
$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $false
$xmlDoc.Load((Resolve-Path $TemplatePath).Path)
$root = $xmlDoc.DocumentElement
$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$ns.AddNamespace("d", "http://v8.1c.ru/8.2/data/spreadsheet")
$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
$ns.AddNamespace("v8ui", "http://v8.1c.ru/8.1/data/ui")
$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
# --- 2. Extract font palette ---
$rawFonts = @()
foreach ($fNode in $root.SelectNodes("d:font", $ns)) {
$rawFonts += @{
Face = $fNode.GetAttribute("faceName")
Size = [int]$fNode.GetAttribute("height")
Bold = $fNode.GetAttribute("bold") -eq "true"
Italic = $fNode.GetAttribute("italic") -eq "true"
Underline = $fNode.GetAttribute("underline") -eq "true"
Strikeout = $fNode.GetAttribute("strikeout") -eq "true"
}
}
# --- 3. Extract line palette ---
$rawLines = @()
foreach ($lNode in $root.SelectNodes("d:line", $ns)) {
$rawLines += @{ Width = [int]$lNode.GetAttribute("width") }
}
# --- 4. Extract format palette ---
$rawFormats = @()
foreach ($fmtNode in $root.SelectNodes("d:format", $ns)) {
$fmt = @{
FontIdx = -1
LB = -1; TB = -1; RB = -1; BB = -1
Width = 0; Height = 0
HA = ""; VA = ""
Wrap = $false; FillType = ""; DataFormat = ""
}
$n = $fmtNode.SelectSingleNode("d:font", $ns)
if ($n) { $fmt.FontIdx = [int]$n.InnerText }
$n = $fmtNode.SelectSingleNode("d:leftBorder", $ns)
if ($n) { $fmt.LB = [int]$n.InnerText }
$n = $fmtNode.SelectSingleNode("d:topBorder", $ns)
if ($n) { $fmt.TB = [int]$n.InnerText }
$n = $fmtNode.SelectSingleNode("d:rightBorder", $ns)
if ($n) { $fmt.RB = [int]$n.InnerText }
$n = $fmtNode.SelectSingleNode("d:bottomBorder", $ns)
if ($n) { $fmt.BB = [int]$n.InnerText }
$n = $fmtNode.SelectSingleNode("d:width", $ns)
if ($n) { $fmt.Width = [int]$n.InnerText }
$n = $fmtNode.SelectSingleNode("d:height", $ns)
if ($n) { $fmt.Height = [int]$n.InnerText }
$n = $fmtNode.SelectSingleNode("d:horizontalAlignment", $ns)
if ($n) { $fmt.HA = $n.InnerText }
$n = $fmtNode.SelectSingleNode("d:verticalAlignment", $ns)
if ($n) { $fmt.VA = $n.InnerText }
$n = $fmtNode.SelectSingleNode("d:textPlacement", $ns)
if ($n -and $n.InnerText -eq "Wrap") { $fmt.Wrap = $true }
$n = $fmtNode.SelectSingleNode("d:fillType", $ns)
if ($n) { $fmt.FillType = $n.InnerText }
$n = $fmtNode.SelectSingleNode("d:format/v8:item/v8:content", $ns)
if ($n) { $fmt.DataFormat = $n.InnerText }
$rawFormats += $fmt
}
function Get-Format {
param([int]$idx)
if ($idx -le 0 -or $idx -gt $rawFormats.Count) { return $null }
return $rawFormats[$idx - 1]
}
# --- 5. Extract columns and default width ---
$colNode = $root.SelectSingleNode("d:columns", $ns)
$totalColumns = [int]$colNode.SelectSingleNode("d:size", $ns).InnerText
$colFormatIndices = @{}
foreach ($ci in $colNode.SelectNodes("d:columnsItem", $ns)) {
$colIdx = [int]$ci.SelectSingleNode("d:index", $ns).InnerText
$fmtIdx = [int]$ci.SelectSingleNode("d:column/d:formatIndex", $ns).InnerText
$colFormatIndices[$colIdx] = $fmtIdx
}
$defaultFmtIdx = 0
$n = $root.SelectSingleNode("d:defaultFormatIndex", $ns)
if ($n) { $defaultFmtIdx = [int]$n.InnerText }
$defaultWidth = 10
if ($defaultFmtIdx -gt 0) {
$defFmt = Get-Format $defaultFmtIdx
if ($defFmt -and $defFmt.Width -gt 0) { $defaultWidth = $defFmt.Width }
}
# Build column width map (1-based col → width), only non-default
$colWidthMap = [ordered]@{}
foreach ($col0 in ($colFormatIndices.Keys | Sort-Object)) {
$fmt = Get-Format $colFormatIndices[$col0]
if ($fmt -and $fmt.Width -gt 0 -and $fmt.Width -ne $defaultWidth) {
$col1 = [string]($col0 + 1)
$colWidthMap.Add($col1, $fmt.Width)
}
}
# --- 6. Extract merges ---
$mergeMap = @{}
foreach ($mNode in $root.SelectNodes("d:merge", $ns)) {
$r = [int]$mNode.SelectSingleNode("d:r", $ns).InnerText
$c = [int]$mNode.SelectSingleNode("d:c", $ns).InnerText
$w = [int]$mNode.SelectSingleNode("d:w", $ns).InnerText
$hNode = $mNode.SelectSingleNode("d:h", $ns)
$h = if ($hNode) { [int]$hNode.InnerText } else { 0 }
$mergeMap["$r,$c"] = @{ W = $w; H = $h }
}
# --- 7. Extract named items ---
$namedAreas = @()
foreach ($niNode in $root.SelectNodes("d:namedItem", $ns)) {
$xsiType = $niNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
if ($xsiType -ne "NamedItemCells") { continue }
$areaNode = $niNode.SelectSingleNode("d:area", $ns)
$areaType = $areaNode.SelectSingleNode("d:type", $ns).InnerText
if ($areaType -ne "Rows") { continue }
$namedAreas += @{
Name = $niNode.SelectSingleNode("d:name", $ns).InnerText
BeginRow = [int]$areaNode.SelectSingleNode("d:beginRow", $ns).InnerText
EndRow = [int]$areaNode.SelectSingleNode("d:endRow", $ns).InnerText
}
}
# --- 8. Extract rows ---
$rowData = @{}
foreach ($riNode in $root.SelectNodes("d:rowsItem", $ns)) {
$rowIdx = [int]$riNode.SelectSingleNode("d:index", $ns).InnerText
$rowNode = $riNode.SelectSingleNode("d:row", $ns)
$indexTo = $rowIdx
$itNode = $riNode.SelectSingleNode("d:indexTo", $ns)
if ($itNode) { $indexTo = [int]$itNode.InnerText }
$rowFmtIdx = 0
$fmtNode = $rowNode.SelectSingleNode("d:formatIndex", $ns)
if ($fmtNode) { $rowFmtIdx = [int]$fmtNode.InnerText }
$isEmpty = $false
$emptyNode = $rowNode.SelectSingleNode("d:empty", $ns)
if ($emptyNode -and $emptyNode.InnerText -eq "true") { $isEmpty = $true }
$cells = @()
if (-not $isEmpty) {
$col = -1
foreach ($cGroup in $rowNode.SelectNodes("d:c", $ns)) {
$iNode = $cGroup.SelectSingleNode("d:i", $ns)
if ($iNode) { $col = [int]$iNode.InnerText }
else { $col++ }
$cContent = $cGroup.SelectSingleNode("d:c", $ns)
if (-not $cContent) { continue }
$cellFmtIdx = 0
$fNode = $cContent.SelectSingleNode("d:f", $ns)
if ($fNode) { $cellFmtIdx = [int]$fNode.InnerText }
$param = $null
$pNode = $cContent.SelectSingleNode("d:parameter", $ns)
if ($pNode) { $param = $pNode.InnerText }
$detail = $null
$dNode = $cContent.SelectSingleNode("d:detailParameter", $ns)
if ($dNode) { $detail = $dNode.InnerText }
$text = $null
$tNode = $cContent.SelectSingleNode("d:tl/v8:item/v8:content", $ns)
if ($tNode) { $text = $tNode.InnerText }
$cells += @{
Col = $col
FormatIdx = $cellFmtIdx
Param = $param
Detail = $detail
Text = $text
}
}
}
for ($r = $rowIdx; $r -le $indexTo; $r++) {
$rowData[$r] = @{
FormatIdx = $rowFmtIdx
Cells = $cells
Empty = $isEmpty
}
}
}
# --- 9. Build style key (ignoring fillType) ---
function Get-BorderDesc {
param($fmt)
if (-not $fmt) { return @{ Border = "none"; Thick = $false } }
$lb = $fmt.LB -ge 0; $tb = $fmt.TB -ge 0
$rb = $fmt.RB -ge 0; $bb = $fmt.BB -ge 0
if (-not $lb -and -not $tb -and -not $rb -and -not $bb) {
return @{ Border = "none"; Thick = $false }
}
$thick = $false
foreach ($bIdx in @($fmt.LB, $fmt.TB, $fmt.RB, $fmt.BB)) {
if ($bIdx -ge 0 -and $bIdx -lt $rawLines.Count -and $rawLines[$bIdx].Width -ge 2) {
$thick = $true; break
}
}
if ($lb -and $tb -and $rb -and $bb) {
return @{ Border = "all"; Thick = $thick }
}
$sides = @()
if ($tb) { $sides += "top" }
if ($bb) { $sides += "bottom" }
if ($lb) { $sides += "left" }
if ($rb) { $sides += "right" }
return @{ Border = ($sides -join ","); Thick = $thick }
}
function Get-StyleKey {
param($fmt)
if (-not $fmt) { return "empty" }
$fi = if ($fmt.FontIdx -ge 0) { $fmt.FontIdx } else { 0 }
$bd = Get-BorderDesc $fmt
return "f=$fi|b=$($bd.Border)|bw=$($bd.Thick)|ha=$($fmt.HA)|va=$($fmt.VA)|wr=$($fmt.Wrap)|df=$($fmt.DataFormat)"
}
# --- 10. Name fonts ---
$fontNames = @{}
$fontDefs = [ordered]@{}
if ($rawFonts.Count -gt 0) {
$fontNames[0] = "default"
$fontDefs["default"] = $rawFonts[0]
}
function Get-FontKey {
param($f)
return "$($f.Face)|$($f.Size)|$($f.Bold)|$($f.Italic)|$($f.Underline)|$($f.Strikeout)"
}
$fontKeyMap = @{}
$fontKeyMap[(Get-FontKey $rawFonts[0])] = "default"
for ($i = 1; $i -lt $rawFonts.Count; $i++) {
$f = $rawFonts[$i]
$df = $rawFonts[0]
# Dedup: if identical font already named, reuse
$fKey = Get-FontKey $f
if ($fontKeyMap.ContainsKey($fKey)) {
$fontNames[$i] = $fontKeyMap[$fKey]
continue
}
$name = $null
if ($f.Face -eq $df.Face -and $f.Size -eq $df.Size) {
if ($f.Bold -and -not $df.Bold -and -not $f.Italic -and -not $f.Underline -and -not $f.Strikeout) {
$name = "bold"
} elseif ($f.Italic -and -not $df.Italic -and -not $f.Bold) {
$name = "italic"
} elseif ($f.Underline -and -not $df.Underline -and -not $f.Bold -and -not $f.Italic) {
$name = "underline"
}
} elseif ($f.Face -eq $df.Face -and $f.Size -gt $df.Size -and $f.Bold) {
$name = "header"
} elseif ($f.Face -eq $df.Face -and $f.Size -lt $df.Size) {
$name = "small"
}
if (-not $name) {
$parts = @()
if ($f.Face -and $f.Face -ne $df.Face) { $parts += $f.Face.ToLower() }
$parts += "$($f.Size)"
if ($f.Bold) { $parts += "bold" }
if ($f.Italic) { $parts += "italic" }
if ($f.Underline) { $parts += "underline" }
if ($f.Strikeout) { $parts += "strikeout" }
$name = $parts -join "-"
}
$baseName = $name; $suffix = 2
while ($fontDefs.Contains($name)) { $name = "$baseName$suffix"; $suffix++ }
$fontNames[$i] = $name
$fontDefs[$name] = $f
$fontKeyMap[$fKey] = $name
}
# --- 11. Collect and name styles ---
$styleKeys = [ordered]@{}
$formatToStyleKey = @{}
foreach ($r in $rowData.Values) {
foreach ($cell in $r.Cells) {
$fmt = Get-Format $cell.FormatIdx
if (-not $fmt) { continue }
$key = Get-StyleKey $fmt
if (-not $styleKeys.Contains($key)) { $styleKeys[$key] = $fmt }
$formatToStyleKey[$cell.FormatIdx] = $key
}
}
function Name-Style {
param($fmt)
if (-not $fmt) { return "default" }
$parts = @()
$fi = if ($fmt.FontIdx -ge 0) { $fmt.FontIdx } else { 0 }
if ($fontNames.ContainsKey($fi) -and $fontNames[$fi] -ne "default") {
$parts += $fontNames[$fi]
}
$bd = Get-BorderDesc $fmt
if ($bd.Border -ne "none") {
if ($bd.Border -eq "all") { $parts += "bordered" }
else { $parts += "border-$($bd.Border)" }
}
if ($fmt.HA -eq "Center") { $parts += "center" }
elseif ($fmt.HA -eq "Right") { $parts += "right" }
if ($fmt.VA -eq "Center") { $parts += "vcenter" }
elseif ($fmt.VA -eq "Top") { $parts += "vtop" }
if ($fmt.Wrap) { $parts += "wrap" }
if ($fmt.DataFormat) { $parts += "fmt" }
if ($parts.Count -eq 0) { return "default" }
return ($parts -join "-")
}
$styleNames = [ordered]@{}
$styleDefs = [ordered]@{}
foreach ($key in $styleKeys.Keys) {
$fmt = $styleKeys[$key]
$name = Name-Style $fmt
$baseName = $name; $suffix = 2
while ($styleDefs.Contains($name)) { $name = "$baseName$suffix"; $suffix++ }
$styleNames[$key] = $name
$sDef = [ordered]@{}
$fi = if ($fmt.FontIdx -ge 0) { $fmt.FontIdx } else { 0 }
if ($fontNames.ContainsKey($fi) -and $fontNames[$fi] -ne "default") {
$sDef["font"] = $fontNames[$fi]
}
if ($fmt.HA) {
$a = switch ($fmt.HA) { "Left" { "left" } "Center" { "center" } "Right" { "right" } }
if ($a) { $sDef["align"] = $a }
}
if ($fmt.VA) {
$a = switch ($fmt.VA) { "Top" { "top" } "Center" { "center" } }
if ($a) { $sDef["valign"] = $a }
}
$bd = Get-BorderDesc $fmt
if ($bd.Border -ne "none") {
$sDef["border"] = $bd.Border
if ($bd.Thick) { $sDef["borderWidth"] = "thick" }
}
if ($fmt.Wrap) { $sDef["wrap"] = $true }
if ($fmt.DataFormat) { $sDef["format"] = $fmt.DataFormat }
$styleDefs[$name] = $sDef
}
function Get-StyleName {
param([int]$fmtIdx)
$key = $formatToStyleKey[$fmtIdx]
if ($key -and $styleNames.Contains($key)) { return $styleNames[$key] }
return "default"
}
# --- 12. Build areas ---
$dslAreas = @()
foreach ($area in $namedAreas) {
$areaRows = @()
for ($globalRow = $area.BeginRow; $globalRow -le $area.EndRow; $globalRow++) {
$rd = $rowData[$globalRow]
if (-not $rd -or $rd.Empty) {
$areaRows += [ordered]@{}
continue
}
$dslRow = [ordered]@{}
# Row height
if ($rd.FormatIdx -gt 0) {
$rowFmt = Get-Format $rd.FormatIdx
if ($rowFmt -and $rowFmt.Height -gt 0) { $dslRow["height"] = $rowFmt.Height }
}
# Separate content cells from gap-fill cells
$contentCells = @()
$gapCells = @()
foreach ($cell in $rd.Cells) {
$hasContent = $cell.Param -or $cell.Text
$hasMerge = $mergeMap.ContainsKey("$globalRow,$($cell.Col)")
if ($hasContent -or $hasMerge) {
$contentCells += $cell
} else {
$gapCells += $cell
}
}
# Detect rowStyle
$rowStyleName = $null
$rowStyleKey = $null
if ($gapCells.Count -gt 0) {
$gapKeys = @{}
foreach ($gc in $gapCells) {
$fmt = Get-Format $gc.FormatIdx
$gapKeys[(Get-StyleKey $fmt)] = $true
}
if ($gapKeys.Count -eq 1) {
$rowStyleKey = @($gapKeys.Keys)[0]
if ($styleNames.Contains($rowStyleKey)) {
$rowStyleName = $styleNames[$rowStyleKey]
}
}
}
if ($rowStyleName -and $rowStyleName -ne "default") { $dslRow["rowStyle"] = $rowStyleName }
# Build cell list
$dslCells = @()
foreach ($cell in ($contentCells | Sort-Object { $_.Col })) {
$dslCell = [ordered]@{ col = $cell.Col + 1 }
# Span/rowspan from merge
$mk = "$globalRow,$($cell.Col)"
if ($mergeMap.ContainsKey($mk)) {
$m = $mergeMap[$mk]
if ($m.W -gt 0) { $dslCell["span"] = $m.W + 1 }
if ($m.H -gt 0) { $dslCell["rowspan"] = $m.H + 1 }
}
# Style
$cellFmt = Get-Format $cell.FormatIdx
$cellStyleKey = Get-StyleKey $cellFmt
if ($rowStyleKey -and $cellStyleKey -eq $rowStyleKey) {
# Inherits rowStyle
} else {
$sn = Get-StyleName $cell.FormatIdx
if ($sn -ne "default" -or -not $rowStyleName) {
$dslCell["style"] = $sn
}
}
# Content
$fillType = if ($cellFmt) { $cellFmt.FillType } else { "" }
if ($cell.Param) {
$dslCell["param"] = $cell.Param
if ($cell.Detail) { $dslCell["detail"] = $cell.Detail }
} elseif ($fillType -eq "Template" -and $cell.Text) {
$dslCell["template"] = $cell.Text
} elseif ($cell.Text) {
$dslCell["text"] = $cell.Text
}
$dslCells += $dslCell
}
if ($dslCells.Count -gt 0) { $dslRow["cells"] = [array]$dslCells }
$areaRows += $dslRow
}
# Compress consecutive empty rows ({}) into { empty = N }
$compressedRows = @()
$emptyRun = 0
foreach ($r in $areaRows) {
if ($r.Count -eq 0) {
$emptyRun++
} else {
if ($emptyRun -gt 0) {
if ($emptyRun -eq 1) { $compressedRows += [ordered]@{} }
else { $compressedRows += [ordered]@{ empty = $emptyRun } }
$emptyRun = 0
}
$compressedRows += $r
}
}
if ($emptyRun -gt 0) {
if ($emptyRun -eq 1) { $compressedRows += [ordered]@{} }
else { $compressedRows += [ordered]@{ empty = $emptyRun } }
}
$dslAreas += [ordered]@{
name = $area.Name
rows = [array]$compressedRows
}
}
# --- 13. Compress columnWidths ---
$compressedWidths = [ordered]@{}
if ($colWidthMap.Count -gt 0) {
$grouped = $colWidthMap.Keys | Group-Object { $colWidthMap[$_] }
foreach ($g in $grouped) {
$width = [int]$g.Name
$cols = @($g.Group | Sort-Object { [int]$_ })
$ranges = @()
$rangeStart = $cols[0]; $rangePrev = $cols[0]
for ($i = 1; $i -lt $cols.Count; $i++) {
if ([int]$cols[$i] -eq [int]$rangePrev + 1) {
$rangePrev = $cols[$i]
} else {
if ($rangeStart -eq $rangePrev) { $ranges += "$rangeStart" }
else { $ranges += "$rangeStart-$rangePrev" }
$rangeStart = $cols[$i]; $rangePrev = $cols[$i]
}
}
if ($rangeStart -eq $rangePrev) { $ranges += "$rangeStart" }
else { $ranges += "$rangeStart-$rangePrev" }
foreach ($range in $ranges) { $compressedWidths[$range] = $width }
}
}
# --- 14. Build fonts output ---
$fontsOut = [ordered]@{}
foreach ($name in $fontDefs.Keys) {
$f = $fontDefs[$name]
$fOut = [ordered]@{ face = $f.Face; size = $f.Size }
if ($f.Bold) { $fOut["bold"] = $true }
if ($f.Italic) { $fOut["italic"] = $true }
if ($f.Underline) { $fOut["underline"] = $true }
if ($f.Strikeout) { $fOut["strikeout"] = $true }
$fontsOut[$name] = $fOut
}
# --- 15. Assemble result ---
$result = [ordered]@{
columns = $totalColumns
defaultWidth = $defaultWidth
}
if ($compressedWidths.Count -gt 0) { $result["columnWidths"] = $compressedWidths }
# Remove empty "default" style
if ($styleDefs.Contains("default") -and $styleDefs["default"].Count -eq 0) {
$styleDefs.Remove("default")
}
# Remove unused styles
$usedStyles = @{}
foreach ($a in $dslAreas) {
foreach ($r in $a.rows) {
if ($r.rowStyle) { $usedStyles[$r.rowStyle] = $true }
if ($r.cells) { foreach ($c in $r.cells) { if ($c.style) { $usedStyles[$c.style] = $true } } }
}
}
$toRemove = @($styleDefs.Keys | Where-Object { -not $usedStyles.ContainsKey($_) })
foreach ($s in $toRemove) { $styleDefs.Remove($s)
}
$result["fonts"] = $fontsOut
$result["styles"] = $styleDefs
$result["areas"] = [array]$dslAreas
# --- 16. Convert to JSON and fix Unicode ---
$json = $result | ConvertTo-Json -Depth 10
# PS 5.1 escapes non-ASCII as \uXXXX — unescape back to UTF-8
$json = [regex]::Replace($json, '\\u([0-9A-Fa-f]{4})', {
param($m)
[char][int]("0x" + $m.Groups[1].Value)
})
# --- 17. Output ---
if ($OutputPath) {
$enc = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText(
(Join-Path (Get-Location) $OutputPath),
$json,
$enc
)
Write-Host "[OK] Decompiled: $OutputPath"
} else {
Write-Output $json
}
Write-Host " Areas: $($namedAreas.Count), Rows: $($rowData.Count), Columns: $totalColumns" -ForegroundColor DarkGray
Write-Host " Fonts: $($fontDefs.Count), Styles: $($styleDefs.Count), Merges: $($mergeMap.Count)" -ForegroundColor DarkGray
@@ -1,727 +0,0 @@
#!/usr/bin/env python3
# mxl-decompile v1.1 — Decompile 1C spreadsheet to JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import sys
from collections import OrderedDict
from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def ci_parse_args(parser, argv=None):
"""parse_args по правилам PS: имена параметров и значения choices регистронезависимы."""
argv = list(sys.argv[1:] if argv is None else argv)
names = {s.lower(): s for a in parser._actions for s in a.option_strings}
for i, tok in enumerate(argv):
if tok.startswith('-') and tok.lower() in names:
argv[i] = names[tok.lower()]
# choices — зеркало [ValidateSet]; канонизируем ДО разбора, иначе argparse отвергнет регистр
choice_map = {}
for a in parser._actions:
if a.choices:
for s in a.option_strings:
choice_map[s] = {str(c).lower(): c for c in a.choices}
for i in range(len(argv) - 1):
m = choice_map.get(argv[i])
if m and argv[i + 1].lower() in m:
argv[i + 1] = m[argv[i + 1].lower()]
return parser.parse_args(argv)
# --- Namespace map ---
NSMAP = {
"d": "http://v8.1c.ru/8.2/data/spreadsheet",
"v8": "http://v8.1c.ru/8.1/data/core",
"v8ui": "http://v8.1c.ru/8.1/data/ui",
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
}
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
def find(node, xpath):
return node.find(xpath, NSMAP)
def findall(node, xpath):
return node.findall(xpath, NSMAP)
def text_of(node):
if node is not None and node.text:
return node.text
return None
def int_of(node, default=0):
if node is not None and node.text:
return int(node.text)
return default
# --- Main ---
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Decompile 1C spreadsheet to JSON", allow_abbrev=False)
parser.add_argument("-TemplatePath", "-Path", required=True, help="Path to Template.xml")
parser.add_argument("-OutputPath", default=None, help="Output JSON path (stdout if omitted)")
args = ci_parse_args(parser)
template_path = args.TemplatePath
output_path = args.OutputPath
# --- 1. Load and parse XML ---
if not os.path.isfile(template_path):
print(f"File not found: {template_path}", file=sys.stderr)
sys.exit(1)
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(template_path, parser_xml)
root = tree.getroot()
# --- 2. Extract font palette ---
raw_fonts = []
for f_node in findall(root, "d:font"):
raw_fonts.append({
"Face": f_node.get("faceName", ""),
"Size": int(f_node.get("height", "0")),
"Bold": f_node.get("bold") == "true",
"Italic": f_node.get("italic") == "true",
"Underline": f_node.get("underline") == "true",
"Strikeout": f_node.get("strikeout") == "true",
})
# --- 3. Extract line palette ---
raw_lines = []
for l_node in findall(root, "d:line"):
raw_lines.append({"Width": int(l_node.get("width", "0"))})
# --- 4. Extract format palette ---
raw_formats = []
for fmt_node in findall(root, "d:format"):
fmt = {
"FontIdx": -1,
"LB": -1, "TB": -1, "RB": -1, "BB": -1,
"Width": 0, "Height": 0,
"HA": "", "VA": "",
"Wrap": False, "FillType": "", "DataFormat": "",
}
n = find(fmt_node, "d:font")
if n is not None and n.text:
fmt["FontIdx"] = int(n.text)
n = find(fmt_node, "d:leftBorder")
if n is not None and n.text:
fmt["LB"] = int(n.text)
n = find(fmt_node, "d:topBorder")
if n is not None and n.text:
fmt["TB"] = int(n.text)
n = find(fmt_node, "d:rightBorder")
if n is not None and n.text:
fmt["RB"] = int(n.text)
n = find(fmt_node, "d:bottomBorder")
if n is not None and n.text:
fmt["BB"] = int(n.text)
n = find(fmt_node, "d:width")
if n is not None and n.text:
fmt["Width"] = int(n.text)
n = find(fmt_node, "d:height")
if n is not None and n.text:
fmt["Height"] = int(n.text)
n = find(fmt_node, "d:horizontalAlignment")
if n is not None and n.text:
fmt["HA"] = n.text
n = find(fmt_node, "d:verticalAlignment")
if n is not None and n.text:
fmt["VA"] = n.text
n = find(fmt_node, "d:textPlacement")
if n is not None and n.text == "Wrap":
fmt["Wrap"] = True
n = find(fmt_node, "d:fillType")
if n is not None and n.text:
fmt["FillType"] = n.text
n = find(fmt_node, "d:format/v8:item/v8:content")
if n is not None and n.text:
fmt["DataFormat"] = n.text
raw_formats.append(fmt)
def get_format(idx):
if idx <= 0 or idx > len(raw_formats):
return None
return raw_formats[idx - 1]
# --- 5. Extract columns and default width ---
col_node = find(root, "d:columns")
total_columns = int_of(find(col_node, "d:size"))
col_format_indices = {}
for ci in findall(col_node, "d:columnsItem"):
col_idx = int_of(find(ci, "d:index"))
fmt_idx = int_of(find(ci, "d:column/d:formatIndex"))
col_format_indices[col_idx] = fmt_idx
default_fmt_idx = 0
n = find(root, "d:defaultFormatIndex")
if n is not None and n.text:
default_fmt_idx = int(n.text)
default_width = 10
if default_fmt_idx > 0:
def_fmt = get_format(default_fmt_idx)
if def_fmt and def_fmt["Width"] > 0:
default_width = def_fmt["Width"]
# Build column width map (1-based col -> width), only non-default
col_width_map = OrderedDict()
for col0 in sorted(col_format_indices.keys()):
fmt = get_format(col_format_indices[col0])
if fmt and fmt["Width"] > 0 and fmt["Width"] != default_width:
col1 = str(col0 + 1)
col_width_map[col1] = fmt["Width"]
# --- 6. Extract merges ---
merge_map = {}
for m_node in findall(root, "d:merge"):
r = int_of(find(m_node, "d:r"))
c = int_of(find(m_node, "d:c"))
w = int_of(find(m_node, "d:w"))
h_node = find(m_node, "d:h")
h = int_of(h_node) if h_node is not None else 0
merge_map[f"{r},{c}"] = {"W": w, "H": h}
# --- 7. Extract named items ---
named_areas = []
for ni_node in findall(root, "d:namedItem"):
xsi_type = ni_node.get(f"{{{XSI_NS}}}type", "")
if xsi_type != "NamedItemCells":
continue
area_node = find(ni_node, "d:area")
area_type_node = find(area_node, "d:type")
area_type = text_of(area_type_node) or ""
if area_type != "Rows":
continue
named_areas.append({
"Name": text_of(find(ni_node, "d:name")) or "",
"BeginRow": int_of(find(area_node, "d:beginRow")),
"EndRow": int_of(find(area_node, "d:endRow")),
})
# --- 8. Extract rows ---
row_data = {}
for ri_node in findall(root, "d:rowsItem"):
row_idx = int_of(find(ri_node, "d:index"))
row_node = find(ri_node, "d:row")
index_to = row_idx
it_node = find(ri_node, "d:indexTo")
if it_node is not None and it_node.text:
index_to = int(it_node.text)
row_fmt_idx = 0
fmt_node = find(row_node, "d:formatIndex")
if fmt_node is not None and fmt_node.text:
row_fmt_idx = int(fmt_node.text)
is_empty = False
empty_node = find(row_node, "d:empty")
if empty_node is not None and empty_node.text == "true":
is_empty = True
cells = []
if not is_empty:
col = -1
for c_group in findall(row_node, "d:c"):
i_node = find(c_group, "d:i")
if i_node is not None and i_node.text:
col = int(i_node.text)
else:
col += 1
c_content = find(c_group, "d:c")
if c_content is None:
continue
cell_fmt_idx = 0
f_node = find(c_content, "d:f")
if f_node is not None and f_node.text:
cell_fmt_idx = int(f_node.text)
param = None
p_node = find(c_content, "d:parameter")
if p_node is not None and p_node.text:
param = p_node.text
detail = None
d_node = find(c_content, "d:detailParameter")
if d_node is not None and d_node.text:
detail = d_node.text
text = None
t_node = find(c_content, "d:tl/v8:item/v8:content")
if t_node is not None and t_node.text:
text = t_node.text
cells.append({
"Col": col,
"FormatIdx": cell_fmt_idx,
"Param": param,
"Detail": detail,
"Text": text,
})
for r in range(row_idx, index_to + 1):
row_data[r] = {
"FormatIdx": row_fmt_idx,
"Cells": cells,
"Empty": is_empty,
}
# --- 9. Build style key (ignoring fillType) ---
def get_border_desc(fmt):
if not fmt:
return {"Border": "none", "Thick": False}
lb = fmt["LB"] >= 0
tb = fmt["TB"] >= 0
rb = fmt["RB"] >= 0
bb = fmt["BB"] >= 0
if not lb and not tb and not rb and not bb:
return {"Border": "none", "Thick": False}
thick = False
for b_idx in [fmt["LB"], fmt["TB"], fmt["RB"], fmt["BB"]]:
if b_idx >= 0 and b_idx < len(raw_lines) and raw_lines[b_idx]["Width"] >= 2:
thick = True
break
if lb and tb and rb and bb:
return {"Border": "all", "Thick": thick}
sides = []
if tb:
sides.append("top")
if bb:
sides.append("bottom")
if lb:
sides.append("left")
if rb:
sides.append("right")
return {"Border": ",".join(sides), "Thick": thick}
def get_style_key(fmt):
if not fmt:
return "empty"
fi = fmt["FontIdx"] if fmt["FontIdx"] >= 0 else 0
bd = get_border_desc(fmt)
return f"f={fi}|b={bd['Border']}|bw={bd['Thick']}|ha={fmt['HA']}|va={fmt['VA']}|wr={fmt['Wrap']}|df={fmt['DataFormat']}"
# --- 10. Name fonts ---
font_names = {}
font_defs = OrderedDict()
if len(raw_fonts) > 0:
font_names[0] = "default"
font_defs["default"] = raw_fonts[0]
def get_font_key(f):
return f"{f['Face']}|{f['Size']}|{f['Bold']}|{f['Italic']}|{f['Underline']}|{f['Strikeout']}"
font_key_map = {}
if len(raw_fonts) > 0:
font_key_map[get_font_key(raw_fonts[0])] = "default"
for i in range(1, len(raw_fonts)):
f = raw_fonts[i]
df = raw_fonts[0]
# Dedup: if identical font already named, reuse
f_key = get_font_key(f)
if f_key in font_key_map:
font_names[i] = font_key_map[f_key]
continue
name = None
if f["Face"] == df["Face"] and f["Size"] == df["Size"]:
if f["Bold"] and not df["Bold"] and not f["Italic"] and not f["Underline"] and not f["Strikeout"]:
name = "bold"
elif f["Italic"] and not df["Italic"] and not f["Bold"]:
name = "italic"
elif f["Underline"] and not df["Underline"] and not f["Bold"] and not f["Italic"]:
name = "underline"
elif f["Face"] == df["Face"] and f["Size"] > df["Size"] and f["Bold"]:
name = "header"
elif f["Face"] == df["Face"] and f["Size"] < df["Size"]:
name = "small"
if not name:
parts = []
if f["Face"] and f["Face"] != df["Face"]:
parts.append(f["Face"].lower())
parts.append(str(f["Size"]))
if f["Bold"]:
parts.append("bold")
if f["Italic"]:
parts.append("italic")
if f["Underline"]:
parts.append("underline")
if f["Strikeout"]:
parts.append("strikeout")
name = "-".join(parts)
base_name = name
suffix = 2
while name in font_defs:
name = f"{base_name}{suffix}"
suffix += 1
font_names[i] = name
font_defs[name] = f
font_key_map[f_key] = name
# --- 11. Collect and name styles ---
style_keys = OrderedDict()
format_to_style_key = {}
for rd in row_data.values():
for cell in rd["Cells"]:
fmt = get_format(cell["FormatIdx"])
if not fmt:
continue
key = get_style_key(fmt)
if key not in style_keys:
style_keys[key] = fmt
format_to_style_key[cell["FormatIdx"]] = key
def name_style(fmt):
if not fmt:
return "default"
parts = []
fi = fmt["FontIdx"] if fmt["FontIdx"] >= 0 else 0
if fi in font_names and font_names[fi] != "default":
parts.append(font_names[fi])
bd = get_border_desc(fmt)
if bd["Border"] != "none":
if bd["Border"] == "all":
parts.append("bordered")
else:
parts.append(f"border-{bd['Border']}")
if fmt["HA"] == "Center":
parts.append("center")
elif fmt["HA"] == "Right":
parts.append("right")
if fmt["VA"] == "Center":
parts.append("vcenter")
elif fmt["VA"] == "Top":
parts.append("vtop")
if fmt["Wrap"]:
parts.append("wrap")
if fmt["DataFormat"]:
parts.append("fmt")
if len(parts) == 0:
return "default"
return "-".join(parts)
style_names = OrderedDict()
style_defs = OrderedDict()
for key in style_keys:
fmt = style_keys[key]
name = name_style(fmt)
base_name = name
suffix = 2
while name in style_defs:
name = f"{base_name}{suffix}"
suffix += 1
style_names[key] = name
s_def = OrderedDict()
fi = fmt["FontIdx"] if fmt["FontIdx"] >= 0 else 0
if fi in font_names and font_names[fi] != "default":
s_def["font"] = font_names[fi]
if fmt["HA"]:
a_map = {"Left": "left", "Center": "center", "Right": "right"}
a = a_map.get(fmt["HA"])
if a:
s_def["align"] = a
if fmt["VA"]:
va_map = {"Top": "top", "Center": "center"}
a = va_map.get(fmt["VA"])
if a:
s_def["valign"] = a
bd = get_border_desc(fmt)
if bd["Border"] != "none":
s_def["border"] = bd["Border"]
if bd["Thick"]:
s_def["borderWidth"] = "thick"
if fmt["Wrap"]:
s_def["wrap"] = True
if fmt["DataFormat"]:
s_def["format"] = fmt["DataFormat"]
style_defs[name] = s_def
def get_style_name(fmt_idx):
key = format_to_style_key.get(fmt_idx)
if key and key in style_names:
return style_names[key]
return "default"
# --- 12. Build areas ---
dsl_areas = []
for area in named_areas:
area_rows = []
for global_row in range(area["BeginRow"], area["EndRow"] + 1):
rd = row_data.get(global_row)
if not rd or rd["Empty"]:
area_rows.append(OrderedDict())
continue
dsl_row = OrderedDict()
# Row height
if rd["FormatIdx"] > 0:
row_fmt = get_format(rd["FormatIdx"])
if row_fmt and row_fmt["Height"] > 0:
dsl_row["height"] = row_fmt["Height"]
# Separate content cells from gap-fill cells
content_cells = []
gap_cells = []
for cell in rd["Cells"]:
has_content = cell["Param"] or cell["Text"]
has_merge = f"{global_row},{cell['Col']}" in merge_map
if has_content or has_merge:
content_cells.append(cell)
else:
gap_cells.append(cell)
# Detect rowStyle
row_style_name = None
row_style_key = None
if len(gap_cells) > 0:
gap_keys = {}
for gc in gap_cells:
fmt = get_format(gc["FormatIdx"])
gap_keys[get_style_key(fmt)] = True
if len(gap_keys) == 1:
row_style_key = list(gap_keys.keys())[0]
if row_style_key in style_names:
row_style_name = style_names[row_style_key]
if row_style_name and row_style_name != "default":
dsl_row["rowStyle"] = row_style_name
# Build cell list
dsl_cells = []
for cell in sorted(content_cells, key=lambda c: c["Col"]):
dsl_cell = OrderedDict()
dsl_cell["col"] = cell["Col"] + 1
# Span/rowspan from merge
mk = f"{global_row},{cell['Col']}"
if mk in merge_map:
m = merge_map[mk]
if m["W"] > 0:
dsl_cell["span"] = m["W"] + 1
if m["H"] > 0:
dsl_cell["rowspan"] = m["H"] + 1
# Style
cell_fmt = get_format(cell["FormatIdx"])
cell_style_key = get_style_key(cell_fmt)
if row_style_key and cell_style_key == row_style_key:
pass # Inherits rowStyle
else:
sn = get_style_name(cell["FormatIdx"])
if sn != "default" or not row_style_name:
dsl_cell["style"] = sn
# Content
fill_type = cell_fmt["FillType"] if cell_fmt else ""
if cell["Param"]:
dsl_cell["param"] = cell["Param"]
if cell["Detail"]:
dsl_cell["detail"] = cell["Detail"]
elif fill_type == "Template" and cell["Text"]:
dsl_cell["template"] = cell["Text"]
elif cell["Text"]:
dsl_cell["text"] = cell["Text"]
dsl_cells.append(dsl_cell)
if len(dsl_cells) > 0:
dsl_row["cells"] = dsl_cells
area_rows.append(dsl_row)
# Compress consecutive empty rows ({}) into { empty = N }
compressed_rows = []
empty_run = 0
for r in area_rows:
if len(r) == 0:
empty_run += 1
else:
if empty_run > 0:
if empty_run == 1:
compressed_rows.append(OrderedDict())
else:
compressed_rows.append(OrderedDict([("empty", empty_run)]))
empty_run = 0
compressed_rows.append(r)
if empty_run > 0:
if empty_run == 1:
compressed_rows.append(OrderedDict())
else:
compressed_rows.append(OrderedDict([("empty", empty_run)]))
dsl_areas.append(OrderedDict([
("name", area["Name"]),
("rows", compressed_rows),
]))
# --- 13. Compress columnWidths ---
compressed_widths = OrderedDict()
if len(col_width_map) > 0:
# Group columns by width
width_to_cols = {}
for col_str, width in col_width_map.items():
width_to_cols.setdefault(width, []).append(col_str)
for width, cols in width_to_cols.items():
cols_sorted = sorted(cols, key=lambda x: int(x))
ranges = []
range_start = cols_sorted[0]
range_prev = cols_sorted[0]
for i in range(1, len(cols_sorted)):
if int(cols_sorted[i]) == int(range_prev) + 1:
range_prev = cols_sorted[i]
else:
if range_start == range_prev:
ranges.append(range_start)
else:
ranges.append(f"{range_start}-{range_prev}")
range_start = cols_sorted[i]
range_prev = cols_sorted[i]
if range_start == range_prev:
ranges.append(range_start)
else:
ranges.append(f"{range_start}-{range_prev}")
for rng in ranges:
compressed_widths[rng] = width
# --- 14. Build fonts output ---
fonts_out = OrderedDict()
for name, f in font_defs.items():
f_out = OrderedDict()
f_out["face"] = f["Face"]
f_out["size"] = f["Size"]
if f["Bold"]:
f_out["bold"] = True
if f["Italic"]:
f_out["italic"] = True
if f["Underline"]:
f_out["underline"] = True
if f["Strikeout"]:
f_out["strikeout"] = True
fonts_out[name] = f_out
# --- 15. Assemble result ---
result = OrderedDict()
result["columns"] = total_columns
result["defaultWidth"] = default_width
if len(compressed_widths) > 0:
result["columnWidths"] = compressed_widths
# Remove empty "default" style
if "default" in style_defs and len(style_defs["default"]) == 0:
del style_defs["default"]
# Remove unused styles
used_styles = set()
for a in dsl_areas:
for r in a["rows"]:
if "rowStyle" in r:
used_styles.add(r["rowStyle"])
if "cells" in r:
for c in r["cells"]:
if "style" in c:
used_styles.add(c["style"])
to_remove = [s for s in style_defs if s not in used_styles]
for s in to_remove:
del style_defs[s]
result["fonts"] = fonts_out
result["styles"] = style_defs
result["areas"] = dsl_areas
# --- 16. Convert to JSON ---
json_str = json.dumps(result, ensure_ascii=False, indent=2)
# --- 17. Output ---
if output_path:
abs_path = os.path.join(os.getcwd(), output_path) if not os.path.isabs(output_path) else output_path
with open(abs_path, "w", encoding="utf-8") as fh:
fh.write(json_str)
print(f"[OK] Decompiled: {output_path}")
else:
print(json_str)
print(f" Areas: {len(named_areas)}, Rows: {len(row_data)}, Columns: {total_columns}", file=sys.stderr)
print(f" Fonts: {len(font_defs)}, Styles: {len(style_defs)}, Merges: {len(merge_map)}", file=sys.stderr)
if __name__ == "__main__":
main()
@@ -1,313 +0,0 @@
# Role DSL — полная справка
Подробная справка по JSON DSL для `/role-compile`. Компактное описание — в [SKILL.md](SKILL.md).
## Структура верхнего уровня
```json
{
"name": "ИмяРоли",
"synonym": "Отображаемое имя роли",
"comment": "",
"setForNewObjects": false,
"setForAttributesByDefault": true,
"independentRightsOfChildObjects": false,
"objects": [ ... ],
"templates": [ ... ]
}
```
- `name` — программное имя роли (обязательно)
- `synonym` — отображаемое имя (по умолчанию = name)
- `comment` — комментарий (по умолчанию пусто)
- Глобальные флаги — по умолчанию `false`, `true`, `false`
## Объекты: два формата
Массив `objects` принимает строки (shorthand) и объекты (полная форма).
### Строковый shorthand
```
"ОбъектМетаданных: @пресет"
"ОбъектМетаданных: Право1, Право2"
```
Примеры:
```json
"objects": [
"Catalog.Номенклатура: @view",
"Document.Реализация: @edit",
"InformationRegister.Цены: Read, Update",
"DataProcessor.Загрузка: @view"
]
```
### Объектная форма (для RLS и переопределений)
```json
{
"name": "Document.Реализация",
"preset": "view",
"rights": { "Delete": false },
"rls": { "Read": "#ДляОбъекта(\"\")" }
}
```
- `preset` — базовый набор прав (`"view"`, `"edit"`)
- `rights` — переопределения: dict `{"Right": true/false}` или массив `["Right1", "Right2"]`
- `rls` — RLS-ограничения: `{"ИмяПрава": "текст условия"}`
## Пресеты — подробные таблицы
Пресеты обозначаются `@` в строковом формате. В объектной форме ключ `preset` без `@`.
### `@view` — просмотр
| Тип объекта | Права |
|-------------|-------|
| Catalog, ExchangePlan, Document, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes, BusinessProcess, Task | Read, View, InputByString |
| InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, Constant, DocumentJournal | Read, View |
| Sequence | Read |
| CommonForm, CommonCommand, Subsystem, FilterCriterion, CommonAttribute | View |
| DataProcessor, Report | Use, View |
| SessionParameter | Get |
| Configuration | ThinClient, WebClient, Output, SaveUserData, MainWindowModeNormal |
### `@edit` — полное редактирование
| Тип объекта | Права |
|-------------|-------|
| Catalog, ExchangePlan, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes | Read, Insert, Update, Delete, View, Edit, InputByString, InteractiveInsert, InteractiveSetDeletionMark, InteractiveClearDeletionMark |
| Document | Read, Insert, Update, Delete, View, Edit, InputByString, Posting, UndoPosting, InteractiveInsert, InteractiveSetDeletionMark, InteractiveClearDeletionMark, InteractivePosting, InteractivePostingRegular, InteractiveUndoPosting, InteractiveChangeOfPosted |
| BusinessProcess | Read, Insert, Update, Delete, View, Edit, InputByString, Start, InteractiveInsert, InteractiveSetDeletionMark, InteractiveClearDeletionMark, InteractiveActivate, InteractiveStart |
| Task | Read, Insert, Update, Delete, View, Edit, InputByString, Execute, InteractiveInsert, InteractiveSetDeletionMark, InteractiveClearDeletionMark, InteractiveActivate, InteractiveExecute |
| InformationRegister, AccumulationRegister, AccountingRegister, Constant | Read, Update, View, Edit |
| DocumentJournal | Read, View |
| Sequence | Read, Update |
| SessionParameter | Get, Set |
| CommonAttribute | View, Edit |
Для сервисов (WebService, HTTPService, IntegrationService) пресеты не определены — используй явные права: `"WebService.Имя: Use"`.
Если пресет не определён для типа объекта — предупреждение с подсказкой доступных.
## Русские синонимы
Скрипт автоматически транслирует русские имена в английские. Можно смешивать: `"Справочник.Контрагенты: Чтение, View"` — работает.
### Типы объектов
| Русский | English |
|---------|---------|
| `Справочник` | Catalog |
| `Документ` | Document |
| `РегистрСведений` | InformationRegister |
| `РегистрНакопления` | AccumulationRegister |
| `РегистрБухгалтерии` | AccountingRegister |
| `РегистрРасчета` | CalculationRegister |
| `Константа` | Constant |
| `ПланСчетов` | ChartOfAccounts |
| `ПланВидовХарактеристик` | ChartOfCharacteristicTypes |
| `ПланВидовРасчета` | ChartOfCalculationTypes |
| `ПланОбмена` | ExchangePlan |
| `БизнесПроцесс` | BusinessProcess |
| `Задача` | Task |
| `Обработка` | DataProcessor |
| `Отчет` | Report |
| `ОбщаяФорма` | CommonForm |
| `ОбщаяКоманда` | CommonCommand |
| `Подсистема` | Subsystem |
| `КритерийОтбора` | FilterCriterion |
| `ЖурналДокументов` | DocumentJournal |
| `Последовательность` | Sequence |
| `ВебСервис` | WebService |
| `HTTPСервис` | HTTPService |
| `СервисИнтеграции` | IntegrationService |
| `ПараметрСеанса` | SessionParameter |
| `ОбщийРеквизит` | CommonAttribute |
| `Конфигурация` | Configuration |
| `Перечисление` | Enum |
### Вложенные типы
| Русский | English |
|---------|---------|
| `Реквизит` | Attribute |
| `СтандартныйРеквизит` | StandardAttribute |
| `ТабличнаяЧасть` | TabularSection |
| `Измерение` | Dimension |
| `Ресурс` | Resource |
| `Команда` | Command |
| `РеквизитАдресации` | AddressingAttribute |
### Права (основные)
| Русский | English |
|---------|---------|
| `Чтение` | Read |
| `Добавление` | Insert |
| `Изменение` | Update |
| `Удаление` | Delete |
| `Просмотр` | View |
| `Редактирование` | Edit |
| `ВводПоСтроке` | InputByString |
| `Проведение` | Posting |
| `ОтменаПроведения` | UndoPosting |
| `Использование` | Use |
| `Получение` | Get |
| `Установка` | Set |
| `Старт` | Start |
| `Выполнение` | Execute |
| `УправлениеИтогами` | TotalsControl |
### Права (интерактивные)
| Русский | English |
|---------|---------|
| `ИнтерактивноеДобавление` | InteractiveInsert |
| `ИнтерактивнаяПометкаУдаления` | InteractiveSetDeletionMark |
| `ИнтерактивноеСнятиеПометкиУдаления` | InteractiveClearDeletionMark |
| `ИнтерактивноеУдаление` | InteractiveDelete |
| `ИнтерактивноеУдалениеПомеченных` | InteractiveDeleteMarked |
| `ИнтерактивноеПроведение` | InteractivePosting |
| `ИнтерактивноеПроведениеНеоперативное` | InteractivePostingRegular |
| `ИнтерактивнаяОтменаПроведения` | InteractiveUndoPosting |
| `ИнтерактивноеИзменениеПроведенных` | InteractiveChangeOfPosted |
| `ИнтерактивныйСтарт` | InteractiveStart |
| `ИнтерактивнаяАктивация` | InteractiveActivate |
| `ИнтерактивноеВыполнение` | InteractiveExecute |
### Права (конфигурация)
| Русский | English |
|---------|---------|
| `Администрирование` | Administration |
| `АдминистрированиеДанных` | DataAdministration |
| `ТонкийКлиент` | ThinClient |
| `ТолстыйКлиент` | ThickClient |
| `ВебКлиент` | WebClient |
| `МобильныйКлиент` | MobileClient |
| `ВнешнееСоединение` | ExternalConnection |
| `Вывод` | Output |
| `СохранениеДанныхПользователя` | SaveUserData |
## Типы объектов без прав в ролях
Следующие типы 1С **не могут** иметь права в ролях (не добавляются в `objects`):
| Тип | Причина |
|-----|---------|
| Enum (Перечисление) | Права наследуются от конфигурации, явное назначение невозможно |
| CommonModule (ОбщийМодуль) | Не имеет собственных прав в роли |
| DefinedType (ОпределяемыйТип) | Тип данных, не объект прав |
| CommonPicture (ОбщаяКартинка) | Ресурс, не объект прав |
| CommonTemplate (ОбщийМакет) | Ресурс, не объект прав |
| Language (Язык) | Конфигурационный элемент |
| FunctionalOption (ФункциональнаяОпция) | Не объект прав |
| FunctionalOptionsParameter | Не объект прав |
| EventSubscription (ПодпискаНаСобытие) | Не объект прав |
| ScheduledJob (РегламентноеЗадание) | Не объект прав |
| StyleItem (ЭлементСтиля) | Ресурс оформления |
## Шаблоны ограничений (RLS templates)
```json
"templates": [
{
"name": "ДляОбъекта(Модификатор)",
"condition": "// текст шаблона\nГДЕ 1=1\n&Модификатор"
}
]
```
- `&` в условии автоматически экранируется в `&amp;` в XML
- Ссылка на шаблон в `rls`: `"#ИмяШаблона(\"параметры\")"` — начинается с `#`
- Параметры шаблона можно передавать пустыми: `#ДляОбъекта("")`
## Примеры
### 1. Простая роль (только пресеты)
```json
{
"name": "ЧтениеНоменклатуры",
"synonym": "Чтение номенклатуры",
"objects": [
"Catalog.Номенклатура: @view",
"Catalog.Контрагенты: @view",
"DataProcessor.Загрузка: @view"
]
}
```
### 2. Роль для регламентного задания
```json
{
"name": "ОбновлениеЦен",
"synonym": "Обновление цен номенклатуры",
"objects": [
"Catalog.Номенклатура: Read",
"Catalog.Валюты: Read",
"InformationRegister.ЦеныНоменклатуры: Read, Update",
"Constant.ОсновнаяВалюта: Read"
]
}
```
### 3. Роль с RLS
```json
{
"name": "ЧтениеДокументовПоОрганизации",
"synonym": "Чтение документов (ограничение по организации)",
"objects": [
"Catalog.Организации: @view",
{
"name": "Document.РеализацияТоваровУслуг",
"preset": "view",
"rls": {
"Read": "#ДляОбъекта(\"\")"
}
}
],
"templates": [
{
"name": "ДляОбъекта(Модификатор)",
"condition": "ГДЕ Организация = &ТекущаяОрганизация"
}
]
}
```
### 4. Роль с русскими синонимами
```json
{
"name": "ПросмотрДанных",
"synonym": "Просмотр данных",
"objects": [
"Справочник.Контрагенты: @view",
"Документ.Реализация: Чтение, Просмотр",
"РегистрСведений.Цены: @edit",
"Обработка.ЗагрузкаДанных: @view"
]
}
```
### 5. Роль с переопределением прав из пресета
```json
{
"name": "ОграниченноеРедактирование",
"synonym": "Редактирование без удаления",
"objects": [
{
"name": "Catalog.Контрагенты",
"preset": "edit",
"rights": { "Delete": false }
}
]
}
```
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "1c-skills-py",
"name": "{{PLUGIN_NAME}}", "version": "2026.8.14+83bb5b6",
"description": "[Python] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент. Linux/Mac или когда PowerShell недоступен.", "description": "[Python] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент.",
"author": { "author": {
"name": "Nikolay Shirokov" "name": "Nikolay Shirokov"
}, },
@@ -27,5 +27,10 @@
"testing", "testing",
"test-automation" "test-automation"
], ],
"skills": "./.claude/skills/" "skills": "./.codex/skills/",
"interface": {
"displayName": "1C Skills (Python)",
"shortDescription": "Python runtime (Linux/Mac/Windows)",
"category": "Development"
}
} }
@@ -24,7 +24,7 @@ allowed-tools:
| `NoValidate` | Пропустить авто-валидацию | | `NoValidate` | Пропустить авто-валидацию |
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-edit.ps1" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1' python ".codex/skills/cf-edit/scripts/cf-edit.py" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
``` ```
## Операции ## Операции
@@ -23,7 +23,7 @@ allowed-tools:
| `OutFile` | Записать результат в файл (UTF-8 BOM) | | `OutFile` | Записать результат в файл (UTF-8 BOM) |
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-info.ps1" -ConfigPath "<путь>" python ".codex/skills/cf-info/scripts/cf-info.py" -ConfigPath "<путь>"
``` ```
## Три режима ## Три режима
+64
View File
@@ -0,0 +1,64 @@
---
name: cf-init
description: Создать пустую конфигурацию 1С (scaffold XML-исходников). Используй когда нужно начать новую конфигурацию с нуля
argument-hint: <Name> [-Synonym <name>] [-OutputDir src]
allowed-tools:
- Bash
- Read
- Glob
---
# /cf-init — Создание пустой конфигурации 1С
Создаёт scaffold исходников пустой конфигурации 1С: `Configuration.xml`, `Languages/Русский.xml`.
## Параметры и команда
| Параметр | Описание |
|----------|----------|
| `Name` | Имя конфигурации (обязат.) |
| `Synonym` | Синоним (= Name если не указан) |
| `OutputDir` | Каталог для создания (default: `src`) |
| `Version` | Версия конфигурации |
| `Vendor` | Поставщик |
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
| `FormatVersion` | Версия формата выгрузки (default: `2.17`) |
Оба параметра задаются от **целевой платформы** — той, на которой конфигурация будет работать, — но по
разным правилам.
`FormatVersion`**не выше** версии формата целевой платформы: 8.3.24 — `2.17`, 8.3.25 — `2.18`,
8.3.26 — `2.19`, 8.3.27 — `2.20`, 8.5 — `2.21`. Ниже брать можно: платформа читает свой формат и любой
более старый, поэтому дефолт `2.17` подходит для всей линейки 8.3.24 и выше.
`CompatibilityMode` влияет на доступную функциональность. Если в задаче не оговорено иное — **равен
целевой платформе** (`Version8_3_27` для 8.3.27); это то же самое, что «Не использовать» в
Конфигураторе, и именно такое значение платформа проставляет новой базе. Более низкий режим берут
осознанно — когда конфигурация должна работать и на старых платформах; он отключает возможности,
появившиеся позже. Выше целевой платформы — нельзя: такая конфигурация загрузится, но работать на ней
не будет.
```powershell
python ".codex/skills/cf-init/scripts/cf-init.py" -Name "МояКонфигурация"
```
## Примеры
```powershell
# Базовая конфигурация
... -Name МояКонфигурация -Synonym "Моя конфигурация" -OutputDir test-tmp/cf
# С версией и поставщиком
... -Name TestCfg -Synonym "Тестовая" -Version "1.0.0.1" -Vendor "Фирма 1С" -OutputDir test-tmp/cf2
# Под платформу 8.3.27 — версия формата и режим совместимости вместе
... -Name TestCfg -FormatVersion 2.20 -CompatibilityMode Version8_3_27 -OutputDir test-tmp/cf3
```
## Верификация
```
/cf-init TestConfig -OutputDir test-tmp/cf
/cf-info test-tmp/cf — проверить созданное
/cf-validate test-tmp/cf — валидировать
```
@@ -1,4 +1,4 @@
# cf-init v1.11 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи) # cf-init v1.14 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -9,14 +9,26 @@ param(
[string]$Vendor, [string]$Vendor,
[string]$CompatibilityMode = "Version8_3_24", [string]$CompatibilityMode = "Version8_3_24",
# Версия формата выгрузки (MDClasses). Её задаёт ПЛАТФОРМА, которой выгружают, и от режима # Версия формата выгрузки (MDClasses). Её задаёт ПЛАТФОРМА, которой выгружают, и от режима
# совместимости она не зависит: 8.3.20-8.3.24 пишут 2.17, 8.3.25 — 2.18, 8.3.26 — 2.19, # совместимости она не зависит. Дефолт 2.17 — нижняя граница проверенного диапазона.
# 8.3.27 — 2.20. Дефолт консервативный: 2.17 читается всеми поддерживаемыми платформами.
[ValidateSet("2.17", "2.18", "2.19", "2.20", "2.21")]
[string]$FormatVersion = "2.17" [string]$FormatVersion = "2.17"
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
# --- Format version ---
# Проверенный диапазон: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница — docs/1c-configuration-spec.md,
# «Лестница версий». Версии ниже 2.17 (платформы 8.3.23 и старше) реальны, поэтому запретом их не
# закрываем: за пределами диапазона — ПРЕДУПРЕЖДЕНИЕ, скаффолд всё равно выпускается. Ошибка — только
# на нечисловое значение: это опечатка, а не версия.
$formatVerifiedMin = "2.17"
$formatVerifiedMax = "2.21"
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
$formatRank = Get-FormatRank $FormatVersion
function Esc-XmlText { function Esc-XmlText {
param([string]$s) param([string]$s)
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми. # Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
@@ -24,6 +36,27 @@ function Esc-XmlText {
} }
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# Проверка версии — ПОСЛЕ настройки кодировки консоли: иначе em-dash в сообщении уедет в вопросы.
# Пишем прямо в stderr, а не Write-Warning: в PS 5.1 предупреждение уходит в stdout, получает
# локализованный префикс и переносится по 80 символов — подстрока в тесте перестаёт находиться.
if ($formatRank -eq 0) {
[Console]::Error.WriteLine("Malformed -FormatVersion '$FormatVersion' (expected N.N, e.g. 2.17)")
exit 1
}
if ($formatRank -lt (Get-FormatRank $formatVerifiedMin) -or $formatRank -gt (Get-FormatRank $formatVerifiedMax)) {
[Console]::Error.WriteLine("WARNING: Format version '$FormatVersion' is outside the tested range $formatVerifiedMin-$formatVerifiedMax — the scaffold is emitted as requested but was not verified on that platform")
}
# «Не использовать» в Конфигураторе хранится как версия ТЕКУЩЕЙ платформы, а не как DontUse:
# свежая база получает Version8_3_<своя>, и ни одна типовая в корпусе DontUse не содержит.
# Само значение легально — платформа принимает его без ошибок, — но не выживает: замерено на
# 8.3.25 и 8.3.27, выгрузка обоих возвращает Version8_3_8. Поэтому предупреждение, а не запрет.
# Сравнение регистронезависимо ЯВНО: в PS -eq таков по умолчанию, в py — нет, и молчаливое
# расхождение портов началось бы прямо здесь.
if ($CompatibilityMode -and $CompatibilityMode.ToLowerInvariant() -eq 'dontuse') {
[Console]::Error.WriteLine("WARNING: CompatibilityMode 'DontUse' is not `"no restrictions`" — the platform stores it as Version8_3_8. For no compatibility restrictions use the target platform version (e.g. Version8_3_27 for 8.3.27).")
}
# --- Resolve output dir --- # --- Resolve output dir ---
if (-not [System.IO.Path]::IsPathRooted($OutputDir)) { if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
$OutputDir = Join-Path (Get-Location).Path $OutputDir $OutputDir = Join-Path (Get-Location).Path $OutputDir
@@ -50,7 +83,9 @@ $co7 = [guid]::NewGuid().ToString()
# --- Mobile functionalities --- # --- Mobile functionalities ---
# Версия формата как число — по ней ниже включаются вставки 2.21. # Версия формата как число — по ней ниже включаются вставки 2.21.
$is221 = (($FormatVersion -match '^(\d+)\.(\d+)$') -and ([int]$Matches[1] * 100 + [int]$Matches[2]) -ge 221) $is221 = ($formatRank -ge 221)
# TextToSpeech приехал раньше остальных вставок 8.5 — своей ступенью, поэтому гейт отдельный.
$is218 = ($formatRank -ge 218)
$mobileFuncs = @( $mobileFuncs = @(
@("Biometrics","true"), @("Location","false"), @("BackgroundLocation","false"), @("Biometrics","true"), @("Location","false"), @("BackgroundLocation","false"),
@@ -68,9 +103,12 @@ $mobileFuncs = @(
@("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"), @("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"),
@("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false") @("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false")
) )
# TextToSpeech — возможность мобильного приложения, добавленная форматом 2.21 (8.5), # TextToSpeech — возможность мобильного приложения, добавленная форматом 2.18 (8.3.25),
# последней в списке. На младших форматах платформа её не пишет. # последней в списке; в 2.21 список не менялся. Замерено выгрузками пустой ИБ шести платформ:
if ($is221) { $mobileFuncs += ,@("TextToSpeech","false") } # 2.13/2.17 — 37 записей без неё, 2.18-2.21 — 38 с ней. Гейт обязателен и в обе стороны:
# на 2.17 тег ломает загрузку XDTO-ошибкой (проверено на 8.3.24), без тега на 2.18+ платформа
# подставит дефолт false и допишет его при выгрузке — то есть разойдётся роундтрип.
if ($is218) { $mobileFuncs += ,@("TextToSpeech","false") }
$mobileXml = "" $mobileXml = ""
foreach ($mf in $mobileFuncs) { foreach ($mf in $mobileFuncs) {
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-init v1.11 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи) # cf-init v1.14 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C configuration.""" """Generates minimal XML source files for a 1C configuration."""
import sys, os, argparse, re, uuid import sys, os, argparse, re, uuid
@@ -50,6 +50,16 @@ def write_xml_file(path, content):
write_utf8_bom(path, text) write_utf8_bom(path, text)
def format_rank(ver):
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
FORMAT_VERIFIED_MIN = "2.17"
FORMAT_VERIFIED_MAX = "2.21"
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -60,13 +70,33 @@ def main():
parser.add_argument('-Version', dest='Version', default='') parser.add_argument('-Version', dest='Version', default='')
parser.add_argument('-Vendor', dest='Vendor', default='') parser.add_argument('-Vendor', dest='Vendor', default='')
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24') parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
# Версия формата выгрузки (MDClasses) — её задаёт ПЛАТФОРМА, а не режим совместимости: # Версия формата выгрузки (MDClasses) — её задаёт ПЛАТФОРМА, а не режим совместимости.
# 8.3.20-8.3.24 пишут 2.17, 8.3.25 — 2.18, 8.3.26 — 2.19, 8.3.27 — 2.20. # Дефолт 2.17 — нижняя граница проверенного диапазона.
# Дефолт консервативный: 2.17 читается всеми платформами. parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17')
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17',
choices=['2.17', '2.18', '2.19', '2.20', '2.21'])
args = ci_parse_args(parser) args = ci_parse_args(parser)
# Проверенный диапазон: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
# docs/1c-configuration-spec.md, «Лестница версий». Версии ниже 2.17 (платформы 8.3.23 и
# старше) реальны, поэтому запретом их не закрываем: за пределами диапазона —
# ПРЕДУПРЕЖДЕНИЕ, скаффолд всё равно выпускается. Ошибка — только на нечисловое значение.
format_rank_value = format_rank(args.FormatVersion)
if format_rank_value == 0:
print(f"Malformed -FormatVersion '{args.FormatVersion}' (expected N.N, e.g. 2.17)", file=sys.stderr)
sys.exit(1)
if not (format_rank(FORMAT_VERIFIED_MIN) <= format_rank_value <= format_rank(FORMAT_VERIFIED_MAX)):
print(f"WARNING: Format version '{args.FormatVersion}' is outside the tested range "
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — the scaffold is emitted as requested "
f"but was not verified on that platform", file=sys.stderr)
# «Не использовать» в Конфигураторе хранится как версия ТЕКУЩЕЙ платформы, а не как DontUse:
# свежая база получает Version8_3_<своя>, и ни одна типовая в корпусе DontUse не содержит.
# Само значение легально — платформа принимает его без ошибок, — но не выживает: замерено на
# 8.3.25 и 8.3.27, выгрузка обоих возвращает Version8_3_8. Поэтому предупреждение, а не запрет.
# Сравнение регистронезависимо ЯВНО: в PS -eq таков по умолчанию, в py — нет, и молчаливое
# расхождение портов началось бы прямо здесь.
if (args.CompatibilityMode or "").lower() == "dontuse":
print("WARNING: CompatibilityMode 'DontUse' is not \"no restrictions\" — the platform stores it as Version8_3_8. For no compatibility restrictions use the target platform version (e.g. Version8_3_27 for 8.3.27).", file=sys.stderr)
name = args.Name name = args.Name
synonym = args.Synonym if args.Synonym else name synonym = args.Synonym if args.Synonym else name
output_dir = args.OutputDir output_dir = args.OutputDir
@@ -91,8 +121,9 @@ def main():
# --- Mobile functionalities --- # --- Mobile functionalities ---
# Версия формата как число — по ней ниже включаются вставки 2.21. # Версия формата как число — по ней ниже включаются вставки 2.21.
_fm = re.match(r'^(\d+)\.(\d+)$', args.FormatVersion) is_221 = format_rank_value >= 221
is_221 = bool(_fm) and int(_fm.group(1)) * 100 + int(_fm.group(2)) >= 221 # TextToSpeech приехал раньше остальных вставок 8.5 — своей ступенью, поэтому гейт отдельный.
is_218 = format_rank_value >= 218
mobile_funcs = [ mobile_funcs = [
("Biometrics","true"), ("Location","false"), ("BackgroundLocation","false"), ("Biometrics","true"), ("Location","false"), ("BackgroundLocation","false"),
@@ -110,9 +141,12 @@ def main():
("DocumentScanning","false"), ("SpeechToText","false"), ("Geofences","false"), ("DocumentScanning","false"), ("SpeechToText","false"), ("Geofences","false"),
("IncomingShareRequests","false"), ("AllIncomingShareRequestsTypesProcessing","false"), ("IncomingShareRequests","false"), ("AllIncomingShareRequestsTypesProcessing","false"),
] ]
# TextToSpeech — возможность мобильного приложения, добавленная форматом 2.21 (8.5), # TextToSpeech — возможность мобильного приложения, добавленная форматом 2.18 (8.3.25),
# последней в списке. На младших форматах платформа её не пишет. # последней в списке; в 2.21 список не менялся. Замерено выгрузками пустой ИБ шести платформ:
if is_221: # 2.13/2.17 — 37 записей без неё, 2.18-2.21 — 38 с ней. Гейт обязателен и в обе стороны:
# на 2.17 тег ломает загрузку XDTO-ошибкой (проверено на 8.3.24), без тега на 2.18+ платформа
# подставит дефолт false и допишет его при выгрузке — то есть разойдётся роундтрип.
if is_218:
mobile_funcs.append(("TextToSpeech", "false")) mobile_funcs.append(("TextToSpeech", "false"))
mobile_xml = "" mobile_xml = ""
@@ -24,6 +24,6 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty" python ".codex/skills/cf-validate/scripts/cf-validate.py" -ConfigPath "upload/cfempty"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty/Configuration.xml" python ".codex/skills/cf-validate/scripts/cf-validate.py" -ConfigPath "upload/cfempty/Configuration.xml"
``` ```
@@ -1,4 +1,4 @@
# cf-validate v1.6 — Validate 1C configuration root structure # cf-validate v1.7 — Validate 1C configuration root structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -89,6 +89,19 @@ $finalize = {
} }
} }
# --- Format version ---
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
$formatVerifiedMin = "2.17"
$formatVerifiedMax = "2.21"
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
# --- Reference tables --- # --- Reference tables ---
$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' $guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$' $identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
@@ -203,11 +216,15 @@ if ($root.NamespaceURI -ne $expectedNs) {
} }
$version = $root.GetAttribute("version") $version = $root.GetAttribute("version")
$versionRank = Get-FormatRank $version
if (-not $version) { if (-not $version) {
Report-Warn "1. Missing version attribute on MetaDataObject" Report-Warn "1. Missing version attribute on MetaDataObject"
} elseif ($version -notin @("2.17", "2.18", "2.19", "2.20", "2.21")) { } elseif ($versionRank -eq 0) {
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27). Report-Error "1. Malformed version '$version' (expected N.N)"
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20 or 2.21)" } elseif ($versionRank -lt (Get-FormatRank $formatVerifiedMin)) {
Report-Warn "1. Format version '$version' is below the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
} elseif ($versionRank -gt (Get-FormatRank $formatVerifiedMax)) {
Report-Warn "1. Format version '$version' is above the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
} }
# Must have Configuration child # Must have Configuration child
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-validate v1.6 — Validate 1C configuration XML structure # cf-validate v1.7 — Validate 1C configuration XML structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages.""" """Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
import sys, os, argparse, re import sys, os, argparse, re
@@ -132,6 +132,20 @@ VALID_ENUM_VALUES = {
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses' EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
# ── Format version ───────────────────────────────────────────
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
FORMAT_VERIFIED_MIN = "2.17"
FORMAT_VERIFIED_MAX = "2.21"
def format_rank(ver):
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
class Reporter: class Reporter:
def __init__(self, max_errors, detailed=False): def __init__(self, max_errors, detailed=False):
@@ -252,11 +266,17 @@ def main():
check1_ok = False check1_ok = False
version = root.get('version', '') version = root.get('version', '')
version_rank = format_rank(version)
if not version: if not version:
r.warn('1. Missing version attribute on MetaDataObject') r.warn('1. Missing version attribute on MetaDataObject')
elif version not in ('2.17', '2.18', '2.19', '2.20', '2.21'): elif version_rank == 0:
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27). r.error(f"1. Malformed version '{version}' (expected N.N)")
r.warn(f"1. Unusual version '{version}' (expected 2.17-2.20 or 2.21)") elif version_rank < format_rank(FORMAT_VERIFIED_MIN):
r.warn(f"1. Format version '{version}' is below the tested range "
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
elif version_rank > format_rank(FORMAT_VERIFIED_MAX):
r.warn(f"1. Format version '{version}' is above the tested range "
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
# Must have Configuration child # Must have Configuration child
cfg_node = None cfg_node = None
@@ -70,31 +70,33 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты" python ".codex/skills/cfe-borrow/scripts/cfe-borrow.py" -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты"
``` ```
## Примеры ## Примеры
```powershell ```powershell
# Заимствовать один объект # Заимствовать один объект
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты" ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты"
# Заимствовать форму (автоматически заимствует родительский объект) # Заимствовать форму (автоматически заимствует родительский объект)
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты.Form.ФормаЭлемента" ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты.Form.ФормаЭлемента"
# Несколько объектов за раз # Несколько объектов за раз
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат" ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат"
# Заимствовать форму с основным реквизитом (реквизиты по DataPath формы) # Заимствовать форму с основным реквизитом (реквизиты по DataPath формы)
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute
# Заимствовать форму с ВСЕМИ реквизитами объекта # Заимствовать форму с ВСЕМИ реквизитами объекта
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All
``` ```
## Верификация ## Верификация
``` ```
/cfe-validate <ExtensionPath> /cfe-validate <ExtensionPath> -ConfigPath <ConfigPath>
``` ```
Конфигурацию-источник передавай и валидатору: заимствованные формы он проверяет по ней.
@@ -1,4 +1,4 @@
# cfe-borrow v1.20 — Borrow objects from configuration into extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы) # cfe-borrow v1.31 — Borrow objects from configuration into extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)][string]$ExtensionPath, [Parameter(Mandatory)][string]$ExtensionPath,
@@ -16,18 +16,38 @@ function Warn([string]$msg) { Write-Host "[WARN] $msg" }
# Form data-binding tags (value = attribute path). A binding survives only if its root # Form data-binding tags (value = attribute path). A binding survives only if its root
# attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the # attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the
# platform rejects the form with "Неверный путь к данным" on load. # platform rejects the form with "Неверный путь к данным" on load.
$script:formBindingDataTags = @('DataPath','TitleDataPath','FooterDataPath','HeaderDataPath','MultipleValueDataPath','MultipleValuePresentDataPath') # RowPictureDataPath тоже путь к данным («Объект.Товары.РасхождениеЗаказ», «Список.DefaultPicture»),
# а не индекс картинки: эталон Конфигуратора сохраняет его с заимствованным основным реквизитом
# и выбрасывает без него — то же правило, что у остальных путей.
$script:formBindingDataTags = @('DataPath','TitleDataPath','FooterDataPath','HeaderDataPath','MultipleValueDataPath','MultipleValuePresentDataPath','RowPictureDataPath')
# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton. # Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton.
$script:formBindingPictureTags = @('RowPictureDataPath','MultipleValuePictureDataPath') $script:formBindingPictureTags = @('MultipleValuePictureDataPath')
# Пути ссылок параметров выбора, которые пришлось вырезать (для предупреждения в конце)
$script:droppedLinks = @()
# id основного реквизита в заимствованной форме — как у Конфигуратора
$script:mainAttrId = "1000001"
# Виды дочерних объектов, которые заимствуются в оболочку поимённо (табличные части — отдельно)
$script:childObjectKinds = @('Attribute','Dimension','Resource','AddressingAttribute')
# Прямые дети <Form>, которые в заимствованную форму не переносятся.
# Структурные секции: AutoCommandBar и ChildItems забираются отдельно, остальные выбрасываются целиком.
$script:formStructuralSections = @('Events','Attributes','Commands','Parameters','CommandInterface')
# Свойства формы, значение которых — имя реквизита формы (реквизиты не заимствуются, ссылка повиснет).
$script:formAttributeRefProps = @('ReportResult','DetailsData','VariantAppearance','GroupList')
# Strip data-binding tags whose root attribute isn't borrowed. # Strip data-binding tags whose root attribute isn't borrowed.
# $keepObjekt=$true (BorrowMainAttribute): keep Объект.* data bindings, strip the rest. # $mainAttrName задан (BorrowMainAttribute): оставить привязки от его имени, остальные снять.
# $keepObjekt=$false (default skeleton): strip all bindings. Picture-path tags are always stripped. # Пусто (скелет без основного реквизита): снять все. Картиночные пути снимаются всегда.
function Strip-FormBindings { function Strip-FormBindings {
param([string]$xml, [bool]$keepObjekt) param([string]$xml, [string]$mainAttrName)
foreach ($tag in $script:formBindingDataTags) { foreach ($tag in $script:formBindingDataTags) {
if ($keepObjekt) { if ($mainAttrName) {
$xml = [regex]::Replace($xml, "\s*<$tag>(?!Объект\.)[^<]*</$tag>", '') # Оставить и «Список.Поле», и путь ровно на сам реквизит («Список» у таблицы формы)
$root = [regex]::Escape($mainAttrName)
$xml = [regex]::Replace($xml, "\s*<$tag>(?!$root(\.|<))[^<]*</$tag>", '')
} else { } else {
$xml = [regex]::Replace($xml, "\s*<$tag>[^<]*</$tag>", '') $xml = [regex]::Replace($xml, "\s*<$tag>[^<]*</$tag>", '')
} }
@@ -38,6 +58,119 @@ function Strip-FormBindings {
return $xml return $xml
} }
# Ссылки параметров выбора (<ChoiceParameterLinks>/<xr:Link>) — привязка особого рода: путь лежит
# в <xr:DataPath> и обычным стриппингом не снимается. Текстовое имя в расширении разрешается только
# если его корень объявлен в <Attributes> самой заимствованной формы; иначе платформа отвергает
# загрузку — «Неверный путь к полю - X». Реквизиты формы не заимствуются никогда, поэтому ссылка на
# них разрешима только через id: Конфигуратор подставляет id реквизита ИСХОДНОЙ формы (эталоны
# Issue66Example4/5/6, JR2433, JR2976, JR49904 — совпадение на шести расширениях). Именно id
# исходной, а не заимствованной: при заимствовании реквизиты перенумеровываются в 1000000+, а
# ссылка продолжает указывать в нумерацию базовой формы.
# Путь на основной реквизит («Объект.X») при заимствованном основном реквизите разрешается текстом
# и остаётся читаемым; без заимствования переводится в «<id>/0:<uuid реквизита объекта>».
# Реквизит, которого в источнике нет, недоступен и по uuid: такую связь вырезаем целиком.
# Пути вида «Items.<Элемент>.CurrentData.<Поле>» не трогаем — их кодировка отдельная.
function Rewrite-ChoiceParameterLinks {
param([string]$xml, $attrUuids, $formAttrIds, [string]$mainAttrName, [bool]$mainAttrBorrowed)
if ($xml -notmatch '<ChoiceParameterLinks>') { return $xml }
$mainPat = if ($mainAttrName) { [regex]::Escape($mainAttrName) } else { $null }
$mainId = if ($mainAttrName -and $formAttrIds.ContainsKey($mainAttrName)) { $formAttrIds[$mainAttrName] } else { "1" }
$xml = [regex]::Replace($xml, '(?s)\s*<xr:Link>.*?</xr:Link>', {
param($m)
$link = $m.Value
$dp = [regex]::Match($link, '<xr:DataPath[^>]*>([^<]+)</xr:DataPath>')
if (-not $dp.Success) { return $link }
$path = $dp.Groups[1].Value
# Путь на основной реквизит формы
if ($mainPat -and $path -match "^${mainPat}\.(.+)$") {
$attrName = $Matches[1]
if ($mainAttrBorrowed) {
# Реквизит объекта разрешается текстом и остаётся читаемым. Стандартное поле
# («Объект.Owner», «Объект.Date») — нет: платформа отвергает «Неверный путь к данным».
# Конфигуратор в этом случае оставляет ссылку на сам реквизит (эталон Issue66Example7_1).
if ($attrUuids.ContainsKey($attrName)) { return $link }
return [regex]::Replace($link, '(<xr:DataPath[^>]*>)[^<]+(</xr:DataPath>)', "`${1}${mainId}`${2}")
}
if ($attrUuids.ContainsKey($attrName)) {
return [regex]::Replace($link, '(<xr:DataPath[^>]*>)[^<]+(</xr:DataPath>)', "`${1}${mainId}/0:$($attrUuids[$attrName])`${2}")
}
return ''
}
# Односегментный путь на реквизит формы — только по id исходной формы
if ($path -notmatch '\.' -and $formAttrIds.ContainsKey($path)) {
return [regex]::Replace($link, '(<xr:DataPath[^>]*>)[^<]+(</xr:DataPath>)', "`${1}$($formAttrIds[$path])`${2}")
}
# Уже непрозрачный путь (форма-источник сама из расширения) — не трогаем
if ($path -match '^\d') { return $link }
# С заимствованным основным реквизитом текстовый путь разрешается: элементы формы на месте,
# а их данные доступны через основной реквизит. Конфигуратор такие пути и оставляет текстом
# (эталон Issue66Example7_1: «Items.Товары.CurrentData.Характеристика» перенесён как есть).
if ($mainAttrBorrowed) { return $link }
# Прочее текстом не разрешается: платформа отвергает загрузку «Неверный путь к полю».
# Сюда попадают «Items.<Элемент>.CurrentData.<Поле>» — их кодировка непрозрачна и по
# имеющимся эталонам не воспроизводима. Связь параметров выбора — удобство подбора, а не
# данные: без неё форма заимствуется и работает, с ней — не грузится вовсе.
$script:droppedLinks += $path
return ''
})
# Опустевший контейнер платформе не нужен
$xml = [regex]::Replace($xml, '(?s)\s*<ChoiceParameterLinks>\s*</ChoiceParameterLinks>', '')
return $xml
}
# Имена ПРЯМЫХ детей собственного <ChildObjects> объекта — для дедупа при повторном
# заимствовании. Текстом это не снять: regex «первый <ChildObjects> до первого </ChildObjects>»
# у объекта с табличными частями обрывается на закрытии первой ТЧ, забирает имена её колонок и
# теряет то, что идёт после неё.
function Get-OwnChildObjectNames {
param([string]$objFile)
$names = @{}
if (-not (Test-Path -LiteralPath $objFile)) { return $names }
$doc = New-Object System.Xml.XmlDocument
$doc.PreserveWhitespace = $false
try { $doc.Load($objFile) } catch { return $names }
$objEl = $null
foreach ($c in $doc.DocumentElement.ChildNodes) {
if ($c.NodeType -eq 'Element') { $objEl = $c; break }
}
if (-not $objEl) { return $names }
$childObjs = $objEl.SelectSingleNode("*[local-name()='ChildObjects']")
if (-not $childObjs) { return $names }
foreach ($child in $childObjs.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue }
$nameNode = $child.SelectSingleNode("*[local-name()='Properties']/*[local-name()='Name']")
if ($nameNode) { $names[$nameNode.InnerText.Trim()] = $true }
}
return $names
}
# Вставка в СОБСТВЕННЫЙ <ChildObjects> объекта. Свой контейнер закрывается в файле последним:
# объект в файле один, а вложенные <ChildObjects> табличных частей закрываются раньше. Замена по
# всем вхождениям раскидывала реквизиты по каждой ТЧ — ps1 рвал XML, py прятал ТЧ внутрь ТЧ.
function Insert-IntoOwnChildObjects {
param([string]$text, [string]$content)
$closeIdx = $text.LastIndexOf('</ChildObjects>')
if ($closeIdx -ge 0) {
return $text.Substring(0, $closeIdx) + "${content}`r`n`t`t" + $text.Substring($closeIdx)
}
# Своего закрывающего тега нет — значит контейнер самозакрытый (детей у него нет, вложенных тоже)
$selfMatches = [regex]::Matches($text, '<ChildObjects\s*/>')
if ($selfMatches.Count -eq 0) { return $text }
$m = $selfMatches[$selfMatches.Count - 1]
return $text.Substring(0, $m.Index) + "<ChildObjects>${content}`r`n`t`t</ChildObjects>" + $text.Substring($m.Index + $m.Length)
}
# --- 1. Resolve paths --- # --- 1. Resolve paths ---
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) { if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath $ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
@@ -209,7 +342,8 @@ $script:generatedTypes = @{
@{ prefix = "AccumulationRegisterRecordKey"; category = "RecordKey" } @{ prefix = "AccumulationRegisterRecordKey"; category = "RecordKey" }
) )
"AccountingRegister" = @( "AccountingRegister" = @(
@{ prefix = "AccountingRegisterRecord"; category = "Record" } @{ prefix = "AccountingRegisterRecord"; category = "Record" }
@{ prefix = "AccountingRegisterExtDimensions"; category = "ExtDimensions" }
@{ prefix = "AccountingRegisterManager"; category = "Manager" } @{ prefix = "AccountingRegisterManager"; category = "Manager" }
@{ prefix = "AccountingRegisterSelection"; category = "Selection" } @{ prefix = "AccountingRegisterSelection"; category = "Selection" }
@{ prefix = "AccountingRegisterList"; category = "List" } @{ prefix = "AccountingRegisterList"; category = "List" }
@@ -223,6 +357,7 @@ $script:generatedTypes = @{
@{ prefix = "CalculationRegisterList"; category = "List" } @{ prefix = "CalculationRegisterList"; category = "List" }
@{ prefix = "CalculationRegisterRecordSet"; category = "RecordSet" } @{ prefix = "CalculationRegisterRecordSet"; category = "RecordSet" }
@{ prefix = "CalculationRegisterRecordKey"; category = "RecordKey" } @{ prefix = "CalculationRegisterRecordKey"; category = "RecordKey" }
@{ prefix = "RecalculationsManager"; category = "Recalcs" }
) )
"ChartOfAccounts" = @( "ChartOfAccounts" = @(
@{ prefix = "ChartOfAccountsObject"; category = "Object" } @{ prefix = "ChartOfAccountsObject"; category = "Object" }
@@ -230,12 +365,15 @@ $script:generatedTypes = @{
@{ prefix = "ChartOfAccountsSelection"; category = "Selection" } @{ prefix = "ChartOfAccountsSelection"; category = "Selection" }
@{ prefix = "ChartOfAccountsList"; category = "List" } @{ prefix = "ChartOfAccountsList"; category = "List" }
@{ prefix = "ChartOfAccountsManager"; category = "Manager" } @{ prefix = "ChartOfAccountsManager"; category = "Manager" }
@{ prefix = "ChartOfAccountsExtDimensionTypes"; category = "ExtDimensionTypes" }
@{ prefix = "ChartOfAccountsExtDimensionTypesRow"; category = "ExtDimensionTypesRow" }
) )
"ChartOfCharacteristicTypes" = @( "ChartOfCharacteristicTypes" = @(
@{ prefix = "ChartOfCharacteristicTypesObject"; category = "Object" } @{ prefix = "ChartOfCharacteristicTypesObject"; category = "Object" }
@{ prefix = "ChartOfCharacteristicTypesRef"; category = "Ref" } @{ prefix = "ChartOfCharacteristicTypesRef"; category = "Ref" }
@{ prefix = "ChartOfCharacteristicTypesSelection"; category = "Selection" } @{ prefix = "ChartOfCharacteristicTypesSelection"; category = "Selection" }
@{ prefix = "ChartOfCharacteristicTypesList"; category = "List" } @{ prefix = "ChartOfCharacteristicTypesList"; category = "List" }
@{ prefix = "Characteristic"; category = "Characteristic" }
@{ prefix = "ChartOfCharacteristicTypesManager"; category = "Manager" } @{ prefix = "ChartOfCharacteristicTypesManager"; category = "Manager" }
) )
"ChartOfCalculationTypes" = @( "ChartOfCalculationTypes" = @(
@@ -245,8 +383,11 @@ $script:generatedTypes = @{
@{ prefix = "ChartOfCalculationTypesList"; category = "List" } @{ prefix = "ChartOfCalculationTypesList"; category = "List" }
@{ prefix = "ChartOfCalculationTypesManager"; category = "Manager" } @{ prefix = "ChartOfCalculationTypesManager"; category = "Manager" }
@{ prefix = "DisplacingCalculationTypes"; category = "DisplacingCalculationTypes" } @{ prefix = "DisplacingCalculationTypes"; category = "DisplacingCalculationTypes" }
@{ prefix = "DisplacingCalculationTypesRow"; category = "DisplacingCalculationTypesRow" }
@{ prefix = "BaseCalculationTypes"; category = "BaseCalculationTypes" } @{ prefix = "BaseCalculationTypes"; category = "BaseCalculationTypes" }
@{ prefix = "BaseCalculationTypesRow"; category = "BaseCalculationTypesRow" }
@{ prefix = "LeadingCalculationTypes"; category = "LeadingCalculationTypes" } @{ prefix = "LeadingCalculationTypes"; category = "LeadingCalculationTypes" }
@{ prefix = "LeadingCalculationTypesRow"; category = "LeadingCalculationTypesRow" }
) )
"BusinessProcess" = @( "BusinessProcess" = @(
@{ prefix = "BusinessProcessObject"; category = "Object" } @{ prefix = "BusinessProcessObject"; category = "Object" }
@@ -254,6 +395,7 @@ $script:generatedTypes = @{
@{ prefix = "BusinessProcessSelection"; category = "Selection" } @{ prefix = "BusinessProcessSelection"; category = "Selection" }
@{ prefix = "BusinessProcessList"; category = "List" } @{ prefix = "BusinessProcessList"; category = "List" }
@{ prefix = "BusinessProcessManager"; category = "Manager" } @{ prefix = "BusinessProcessManager"; category = "Manager" }
@{ prefix = "BusinessProcessRoutePointRef"; category = "RoutePointRef" }
) )
"Task" = @( "Task" = @(
@{ prefix = "TaskObject"; category = "Object" } @{ prefix = "TaskObject"; category = "Object" }
@@ -320,6 +462,16 @@ $typesWithChildObjects = @(
# CommonModule properties to copy from source # CommonModule properties to copy from source
$commonModuleProps = @("Global","ClientManagedApplication","Server","ExternalConnection","ClientOrdinaryApplication","ServerCall") $commonModuleProps = @("Global","ClientManagedApplication","Server","ExternalConnection","ClientOrdinaryApplication","ServerCall")
# Свойства объекта, от которых зависит существование стандартного поля: без них платформа
# отвергает загрузку — «Неверный путь к данным». Конфигуратор переносит ровно их (эталоны
# Issue66Example7_1 и Issue66Example2). Проверено сплошным прогоном по типам: у регистра сведений
# без InformationRegisterPeriodicity не разрешается «Запись.Period».
$script:typeGateProps = @{
"InformationRegister" = @("InformationRegisterPeriodicity","WriteMode")
}
# Владельцы справочника — список <xr:Item>, а не скаляр: переносится фрагментом, как __TypeXml
$script:typesWithOwners = @("Catalog","ChartOfCharacteristicTypes")
# Standard system fields to skip when collecting DataPath references # Standard system fields to skip when collecting DataPath references
$script:standardFields = @("Code","Description","Ref","Parent","DeletionMark","Predefined","IsFolder","LineNumber","RowsCount","PredefinedDataName") $script:standardFields = @("Code","Description","Ref","Parent","DeletionMark","Predefined","IsFolder","LineNumber","RowsCount","PredefinedDataName")
@@ -440,6 +592,37 @@ if ($BorrowMainAttribute) {
} }
# --- 10. Helper: read source object XML --- # --- 10. Helper: read source object XML ---
# Имена реквизитов исходного объекта → uuid. Нужны для непрозрачной формы пути в ссылках
# параметров выбора (см. Rewrite-ChoiceParameterLinks).
function Get-SourceAttributeUuids {
param([string]$typeName, [string]$objName)
$result = @{}
$dirName = $childTypeDirMap[$typeName]
if (-not $dirName) { return $result }
$srcFile = Join-Path (Join-Path $cfgDir $dirName) "${objName}.xml"
if (-not (Test-Path $srcFile)) { return $result }
$doc = New-Object System.Xml.XmlDocument
$doc.PreserveWhitespace = $false
$doc.Load($srcFile)
$objEl = $null
foreach ($c in $doc.DocumentElement.ChildNodes) {
if ($c.NodeType -eq 'Element') { $objEl = $c; break }
}
if (-not $objEl) { return $result }
$childObjects = $objEl.SelectSingleNode("*[local-name()='ChildObjects']")
if (-not $childObjects) { return $result }
foreach ($child in $childObjects.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue }
if ($child.LocalName -notin @('Attribute','TabularSection')) { continue }
$uuid = $child.GetAttribute("uuid")
$nameNode = $child.SelectSingleNode("*[local-name()='Properties']/*[local-name()='Name']")
if ($uuid -and $nameNode) { $result[$nameNode.InnerText.Trim()] = $uuid }
}
return $result
}
function Read-SourceObject { function Read-SourceObject {
param([string]$typeName, [string]$objName) param([string]$typeName, [string]$objName)
@@ -499,6 +682,19 @@ function Read-SourceObject {
$srcProps["__TypeXml"] = [regex]::Replace($typeNode.OuterXml, '\s+xmlns(?::\w+)?="[^"]*"', '') $srcProps["__TypeXml"] = [regex]::Replace($typeNode.OuterXml, '\s+xmlns(?::\w+)?="[^"]*"', '')
} }
} }
# Владельцы: стандартное поле «Owner» появляется у справочника, только если задан Owners
if ($script:typesWithOwners -ccontains $typeName) {
$ownersNode = $propsNode.SelectSingleNode("md:Owners", $srcNs)
if ($ownersNode -and $ownersNode.HasChildNodes) {
$srcProps["__OwnersXml"] = [regex]::Replace($ownersNode.OuterXml, '\s+xmlns(?::\w+)?="[^"]*"', '')
}
}
# Скалярные свойства, включающие стандартные поля своего типа
foreach ($gp in @($script:typeGateProps[$typeName])) {
if (-not $gp) { continue }
$gpNode = $propsNode.SelectSingleNode("md:${gp}", $srcNs)
if ($gpNode) { $srcProps[$gp] = $gpNode.InnerText.Trim() }
}
} }
# Whether the platform emits <ChildObjects> for this type — the source object is the ground truth # Whether the platform emits <ChildObjects> for this type — the source object is the ground truth
@@ -619,30 +815,56 @@ function Borrow-Form {
# (e.g. a 2.13 form inside a 2.17 extension). The platform itself upgrades the form to the root version. # (e.g. a 2.13 form inside a 2.17 extension). The platform itself upgrades the form to the root version.
$formVersion = $script:formatVersion $formVersion = $script:formatVersion
# Find direct children: form properties, AutoCommandBar, ChildItems # Find direct children: form properties, AutoCommandBar, ChildItems.
# Секции формы отбираются по имени, а не по позиции: свойства лежат и до, и после <CommandSet>
# (корпусная проверка: у всех 794 форм документов ERP с CommandSet он стоит раньше AutoCommandBar,
# а AutoTime/UsePostingMode/RepostOnWrite — после него). Позиционная отсечка теряла весь хвост,
# и платформа молча подставляла дефолты вместо потерянных свойств.
$srcAutoCmd = $null $srcAutoCmd = $null
$srcChildItems = $null $srcChildItems = $null
$formProps = @() $formProps = @()
$reachedVisual = $false
foreach ($fc in $srcFormEl.ChildNodes) { foreach ($fc in $srcFormEl.ChildNodes) {
if ($fc.NodeType -ne 'Element') { continue } if ($fc.NodeType -ne 'Element') { continue }
if ($fc.LocalName -eq 'AutoCommandBar' -and -not $srcAutoCmd) { if ($fc.LocalName -eq 'AutoCommandBar' -and -not $srcAutoCmd) {
$reachedVisual = $true; $srcAutoCmd = $fc; continue $srcAutoCmd = $fc; continue
} }
if ($fc.LocalName -eq 'ChildItems' -and -not $srcChildItems) { if ($fc.LocalName -eq 'ChildItems' -and -not $srcChildItems) {
$reachedVisual = $true; $srcChildItems = $fc; continue $srcChildItems = $fc; continue
}
if ($fc.LocalName -eq 'Events' -or $fc.LocalName -eq 'Attributes' -or $fc.LocalName -eq 'Commands' -or $fc.LocalName -eq 'Parameters' -or $fc.LocalName -eq 'CommandSet') {
$reachedVisual = $true; continue
}
if (-not $reachedVisual) {
$formProps += $fc.OuterXml
} }
# Структурные секции: в расширении их содержимое недействительно (обработчики, команды и
# параметры базовой формы, ссылки командного интерфейса на команды базовой конфигурации).
if ($script:formStructuralSections -ccontains $fc.LocalName) { continue }
# Свойства, значение которых — имя реквизита формы. Реквизиты в заимствованную форму не
# переносятся, поэтому Конфигуратор такие свойства выбрасывает (проверено на форме отчёта:
# ReportResult и DetailsData выброшены, CustomSettingsFolder — имя элемента — сохранён).
if ($script:formAttributeRefProps -ccontains $fc.LocalName) { continue }
$formProps += $fc.OuterXml
} }
# Get OuterXml and strip redundant namespace redeclarations (they're on root <Form>) # Get OuterXml and strip redundant namespace redeclarations (they're on root <Form>)
$nsStripPattern = '\s+xmlns(?::\w+)?="[^"]*"' $nsStripPattern = '\s+xmlns(?::\w+)?="[^"]*"'
# Основной реквизит исходной формы: его имя — корень путей к данным, которые нужно сохранить
# («Объект.» у формы объекта, «Список.» у формы списка, «Запись.» у формы записи регистра)
# Имя основного реквизита источника нужно в обоих режимах: по нему опознаётся корень путей
# в ссылках параметров выбора. А $mainAttrName управляет вырезанием привязок и потому остаётся
# пустым в скелетном режиме — там привязки снимаются все.
$srcMainInfo = Get-MainAttributeInfo $srcFormEl $nsStripPattern
$srcMainAttrName = if ($srcMainInfo) { $srcMainInfo.Name } else { "" }
$formAttrIds = Get-FormAttributeIds $srcFormEl
$mainAttrInfo = if ($BorrowMainAttr) { $srcMainInfo } else { $null }
$mainAttrName = if ($mainAttrInfo) { $mainAttrInfo.Name } else { "" }
if ($BorrowMainAttr -and -not $mainAttrInfo) {
Warn " У формы нет основного реквизита — -BorrowMainAttribute проигнорирован"
}
# uuid реквизитов объекта нужны ровно там, где основной реквизит НЕ попал в форму:
# только тогда путь «<основной>.X» переводится в непрозрачный вид
# Имена реквизитов объекта нужны в обоих режимах: без заимствования — чтобы построить
# непрозрачный путь, с заимствованием — чтобы отличить реквизит (разрешается текстом) от
# стандартного поля (не разрешается)
$srcAttrUuids = Get-SourceAttributeUuids $typeName $objName
# AutoCommandBar: keep ChildItems (buttons with CommandName→0), Autofill→false # AutoCommandBar: keep ChildItems (buttons with CommandName→0), Autofill→false
$autoCmdXml = "" $autoCmdXml = ""
if ($srcAutoCmd) { if ($srcAutoCmd) {
@@ -650,10 +872,13 @@ function Borrow-Form {
$autoCmdXml = [regex]::Replace($autoCmdXml, $nsStripPattern, '') $autoCmdXml = [regex]::Replace($autoCmdXml, $nsStripPattern, '')
$autoCmdXml = [regex]::Replace($autoCmdXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>') $autoCmdXml = [regex]::Replace($autoCmdXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
$autoCmdXml = $autoCmdXml -replace '<Autofill>true</Autofill>', '<Autofill>false</Autofill>' $autoCmdXml = $autoCmdXml -replace '<Autofill>true</Autofill>', '<Autofill>false</Autofill>'
# Strip ExcludedCommand (references to standard commands invalid in extension) # Вложенный CommandSet выбрасывается целиком, а не опустошается: Конфигуратор в заимствованной
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '') # форме оставляет только корневой (тот идёт свойством формы, здесь его нет).
$autoCmdXml = [regex]::Replace($autoCmdXml, '(?s)\s*<CommandSet>.*?</CommandSet>', '')
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<CommandSet/>', '')
# Strip data-binding tags whose root attribute isn't borrowed # Strip data-binding tags whose root attribute isn't borrowed
$autoCmdXml = Strip-FormBindings $autoCmdXml ([bool]$BorrowMainAttr) $autoCmdXml = Strip-FormBindings $autoCmdXml $mainAttrName
$autoCmdXml = Rewrite-ChoiceParameterLinks $autoCmdXml $srcAttrUuids $formAttrIds $srcMainAttrName ([bool]$mainAttrInfo)
} }
# ChildItems: copy full tree, clean up base-config references # ChildItems: copy full tree, clean up base-config references
@@ -665,9 +890,11 @@ function Borrow-Form {
$childItemsXml = [regex]::Replace($childItemsXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>') $childItemsXml = [regex]::Replace($childItemsXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
# Strip data-binding tags whose root attribute isn't borrowed # Strip data-binding tags whose root attribute isn't borrowed
# (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*/RowPicture*) # (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*/RowPicture*)
$childItemsXml = Strip-FormBindings $childItemsXml ([bool]$BorrowMainAttr) $childItemsXml = Strip-FormBindings $childItemsXml $mainAttrName
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension) $childItemsXml = Rewrite-ChoiceParameterLinks $childItemsXml $srcAttrUuids $formAttrIds $srcMainAttrName ([bool]$mainAttrInfo)
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '') # Вложенные CommandSet (у таблиц, полей табличного документа и т.п.) — целиком, см. выше
$childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*<CommandSet>.*?</CommandSet>', '')
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<CommandSet/>', '')
# Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID) # Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID)
$childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*<TypeLink>\s*<xr:DataPath>Items\.[^<]*</xr:DataPath>.*?</TypeLink>', '') $childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*<TypeLink>\s*<xr:DataPath>Items\.[^<]*</xr:DataPath>.*?</TypeLink>', '')
# Strip element-level Events (base form handlers not in extension) # Strip element-level Events (base form handlers not in extension)
@@ -884,17 +1111,9 @@ function Borrow-Form {
$formXmlSb.Append("`r`n") | Out-Null $formXmlSb.Append("`r`n") | Out-Null
} }
# Attributes: empty or with MainAttribute when BorrowMainAttr # Attributes: empty or with MainAttribute when BorrowMainAttr
if ($BorrowMainAttr) { if ($BorrowMainAttr -and $mainAttrInfo) {
$objTypePrefix = ""
$gtList = $script:generatedTypes[$typeName]
if ($gtList) { foreach ($g in $gtList) { if ($g.category -eq "Object") { $objTypePrefix = $g.prefix; break } } }
$mainAttrType = "cfg:${objTypePrefix}.${objName}"
$formXmlSb.Append("`t<Attributes>`r`n") | Out-Null $formXmlSb.Append("`t<Attributes>`r`n") | Out-Null
$formXmlSb.Append("`t`t<Attribute name=`"Объект`" id=`"1000001`">`r`n") | Out-Null $formXmlSb.Append("`t`t$($mainAttrInfo.Xml)`r`n") | Out-Null
$formXmlSb.Append("`t`t`t<Type><v8:Type>${mainAttrType}</v8:Type></Type>`r`n") | Out-Null
$formXmlSb.Append("`t`t`t<MainAttribute>true</MainAttribute>`r`n") | Out-Null
$formXmlSb.Append("`t`t`t<SavedData>true</SavedData>`r`n") | Out-Null
$formXmlSb.Append("`t`t</Attribute>`r`n") | Out-Null
$formXmlSb.Append("`t</Attributes>") | Out-Null $formXmlSb.Append("`t</Attributes>") | Out-Null
} else { } else {
$formXmlSb.Append("`t<Attributes/>") | Out-Null $formXmlSb.Append("`t<Attributes/>") | Out-Null
@@ -928,13 +1147,15 @@ function Borrow-Form {
} }
# BaseForm Attributes: same as main section # BaseForm Attributes: same as main section
if ($BorrowMainAttr) { if ($BorrowMainAttr -and $mainAttrInfo) {
$formXmlSb.Append("`t`t<Attributes>`r`n") | Out-Null $formXmlSb.Append("`t`t<Attributes>`r`n") | Out-Null
$formXmlSb.Append("`t`t`t<Attribute name=`"Объект`" id=`"1000001`">`r`n") | Out-Null # В BaseForm та же секция на уровень глубже — приём переиндентации тот же, что у ChildItems
$formXmlSb.Append("`t`t`t`t<Type><v8:Type>${mainAttrType}</v8:Type></Type>`r`n") | Out-Null $maLines = $mainAttrInfo.Xml -split "`r?`n"
$formXmlSb.Append("`t`t`t`t<MainAttribute>true</MainAttribute>`r`n") | Out-Null for ($li = 0; $li -lt $maLines.Count; $li++) {
$formXmlSb.Append("`t`t`t`t<SavedData>true</SavedData>`r`n") | Out-Null if ($li -eq 0) { $formXmlSb.Append("`t`t`t$($maLines[$li])") | Out-Null }
$formXmlSb.Append("`t`t`t</Attribute>`r`n") | Out-Null else { $formXmlSb.Append("`t$($maLines[$li])") | Out-Null }
$formXmlSb.Append("`r`n") | Out-Null
}
$formXmlSb.Append("`t`t</Attributes>") | Out-Null $formXmlSb.Append("`t`t</Attributes>") | Out-Null
} else { } else {
$formXmlSb.Append("`t`t<Attributes/>") | Out-Null $formXmlSb.Append("`t`t<Attributes/>") | Out-Null
@@ -960,6 +1181,11 @@ function Borrow-Form {
$formXmlText = ($formXmlText -replace "`r`n", "`n") -replace "`n", "`r`n" $formXmlText = ($formXmlText -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($formXmlFile, $formXmlText, $enc) [System.IO.File]::WriteAllText($formXmlFile, $formXmlText, $enc)
Info " Created: $formXmlFile" Info " Created: $formXmlFile"
if ($script:droppedLinks.Count -gt 0) {
$uniq = @($script:droppedLinks | Sort-Object -Unique)
Warn " Вырезано связей параметров выбора: $($uniq.Count) — путь не разрешается в расширении: $($uniq -join ', ')"
$script:droppedLinks = @()
}
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must # 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
# not clobber user code added to the form module). # not clobber user code added to the form module).
@@ -1120,8 +1346,46 @@ function Build-InternalInfoXml {
} }
# --- 11b. Collect DataPath references from source Form.xml --- # --- 11b. Collect DataPath references from source Form.xml ---
# --- 11b1. Основной реквизит исходной формы ---
# Переносится ЦЕЛИКОМ, а не собирается из констант: имя, тип и состав детей зависят от вида формы.
# У формы объекта это «Объект»/<Тип>Object + SavedData/UseAlways/Columns, у формы списка —
# «Список»/DynamicList + Settings, у формы записи регистра — «Запись»/RecordManager + SavedData.
# Синтез фиксированного набора давал для необъектных форм «Исключение XDTO» при загрузке.
# Конфигуратор меняет у скопированного реквизита только id (эталоны Issue64UtB, Issue66Example2).
# Имена реквизитов ИСХОДНОЙ формы → их id. Ссылки параметров выбора адресуют реквизит формы
# именно по id базовой формы (см. Rewrite-ChoiceParameterLinks).
function Get-FormAttributeIds {
param($formEl)
$result = @{}
$attrs = $formEl.SelectSingleNode("*[local-name()='Attributes']")
if (-not $attrs) { return $result }
foreach ($a in $attrs.ChildNodes) {
if ($a.NodeType -ne 'Element' -or $a.LocalName -ne 'Attribute') { continue }
$nm = $a.GetAttribute("name"); $id = $a.GetAttribute("id")
if ($nm -and $id) { $result[$nm] = $id }
}
return $result
}
function Get-MainAttributeInfo {
param($formEl, [string]$nsStripPattern)
$mainAttr = $formEl.SelectSingleNode("*[local-name()='Attributes']/*[local-name()='Attribute'][*[local-name()='MainAttribute']='true']")
if (-not $mainAttr) { return $null }
$xml = [regex]::Replace($mainAttr.OuterXml, $nsStripPattern, '')
# id заменяется только в открывающем теге самого реквизита — у вложенных элементов свои
$xml = [regex]::Replace($xml, '^(<Attribute\s[^>]*?)id="[^"]*"', "`${1}id=`"$script:mainAttrId`"")
return @{ Name = $mainAttr.GetAttribute("name"); Xml = $xml }
}
function Collect-FormDataPaths { function Collect-FormDataPaths {
param([string]$formXmlPath) param([string]$formXmlPath, [string]$mainAttrName)
# Корень путей — имя основного реквизита формы: «Объект» у формы объекта, «Список» у формы
# списка, «Запись» у формы записи регистра. Зашитый «Объект» не находил ничего у необъектных
# форм, и в оболочку не заимствовалось ни одного дочернего объекта.
$root = [regex]::Escape($mainAttrName)
$enc = New-Object System.Text.UTF8Encoding($true) $enc = New-Object System.Text.UTF8Encoding($true)
$content = [System.IO.File]::ReadAllText($formXmlPath, $enc) $content = [System.IO.File]::ReadAllText($formXmlPath, $enc)
@@ -1132,7 +1396,7 @@ function Collect-FormDataPaths {
# Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*) # Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*)
# for Объект.* references — picture-path tags carry picture indices, not data attributes. # for Объект.* references — picture-path tags carry picture indices, not data attributes.
foreach ($tag in $script:formBindingDataTags) { foreach ($tag in $script:formBindingDataTags) {
$bms = [regex]::Matches($content, "<$tag>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</$tag>") $bms = [regex]::Matches($content, "<$tag>[^<]*\b$root\.(\w+(?:\.\w+)*)</$tag>")
foreach ($m in $bms) { foreach ($m in $bms) {
$path = $m.Groups[1].Value $path = $m.Groups[1].Value
$segments = $path.Split(".") $segments = $path.Split(".")
@@ -1150,7 +1414,7 @@ function Collect-FormDataPaths {
# Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance # Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance
# fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too. # fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too.
$fieldMatches = [regex]::Matches($content, "<Field>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</Field>") $fieldMatches = [regex]::Matches($content, "<Field>[^<]*\b$root\.(\w+(?:\.\w+)*)</Field>")
foreach ($m in $fieldMatches) { foreach ($m in $fieldMatches) {
$path = $m.Groups[1].Value $path = $m.Groups[1].Value
$segments = $path.Split(".") $segments = $path.Split(".")
@@ -1164,6 +1428,30 @@ function Collect-FormDataPaths {
} }
} }
# Also scan <AdditionalColumns table="Объект.X"> — доп. колонки табличной части, объявленные в
# самой форме (напр. Объект.Товары.Артикул). Такая ТЧ может больше нигде на форме не встречаться,
# и без её заимствования платформа отвергает форму: «Неверный путь к данным».
$acMatches = [regex]::Matches($content, "<AdditionalColumns table=`"$root\.(\w+)`"")
foreach ($m in $acMatches) {
$seg0 = $m.Groups[1].Value
if ($script:standardFields -contains $seg0) { continue }
$firstLevel[$seg0] = $true
}
# Текст запроса динамического списка — такое же место ссылки на реквизиты объекта, как DataPath.
# Конфигуратор заимствует всё, что упомянуто в запросе: на эталоне Issue66Example2 это 21 из 27
# дочерних объектов, совпадение с ним точное в обе стороны. У списка без ручного запроса
# (<QueryText> нет) заимствуется только видимое на форме — эталон Issue66Example3.
# Разбирать язык запросов не нужно: имена-кандидаты отфильтрует Resolve-SourceAttributes по
# реальному составу объекта, поэтому лишние слова из запроса безвредны.
foreach ($qm in [regex]::Matches($content, '(?s)<QueryText>(.*?)</QueryText>')) {
foreach ($w in [regex]::Matches($qm.Groups[1].Value, '[\w]+')) {
$word = $w.Value
if ($script:standardFields -contains $word) { continue }
$firstLevel[$word] = $true
}
}
# Deduplicate deep paths # Deduplicate deep paths
$seen = @{} $seen = @{}
$uniqueDeep = @() $uniqueDeep = @()
@@ -1214,7 +1502,11 @@ function Resolve-SourceAttributes {
foreach ($child in $childObjs.ChildNodes) { foreach ($child in $childObjs.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue } if ($child.NodeType -ne 'Element') { continue }
if ($child.LocalName -eq 'Attribute') { # Реквизит объекта, измерение и ресурс регистра — один и тот же вид дочернего объекта с
# точки зрения заимствования, различается только имя элемента. Конфигуратор переносит их
# своим видом (эталон Issue66Example2: у регистра <Dimension> x3 и <Resource>), поэтому вид
# запоминается и выпускается как есть — иначе измерение уехало бы в файл как <Attribute>.
if ($script:childObjectKinds -ccontains $child.LocalName) {
$nameNode = $child.SelectSingleNode("md:Properties/md:Name", $srcNs) $nameNode = $child.SelectSingleNode("md:Properties/md:Name", $srcNs)
if (-not $nameNode) { continue } if (-not $nameNode) { continue }
$attrName = $nameNode.InnerText $attrName = $nameNode.InnerText
@@ -1226,7 +1518,7 @@ function Resolve-SourceAttributes {
# Strip namespace declarations from Type # Strip namespace declarations from Type
$typeXml = [regex]::Replace($typeXml, '\s+xmlns(?::\w+)?="[^"]*"', '') $typeXml = [regex]::Replace($typeXml, '\s+xmlns(?::\w+)?="[^"]*"', '')
$attrs += @{ Name = $attrName; Uuid = $uuid; TypeXml = $typeXml } $attrs += @{ Name = $attrName; Uuid = $uuid; TypeXml = $typeXml; Kind = $child.LocalName }
} }
elseif ($child.LocalName -eq 'TabularSection') { elseif ($child.LocalName -eq 'TabularSection') {
$nameNode = $child.SelectSingleNode("md:Properties/md:Name", $srcNs) $nameNode = $child.SelectSingleNode("md:Properties/md:Name", $srcNs)
@@ -1276,8 +1568,16 @@ function Resolve-SourceAttributes {
$extraProps = [ordered]@{} $extraProps = [ordered]@{}
$propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs) $propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs)
if ($propsNode) { if ($propsNode) {
$propsToExtract = @("Hierarchical","FoldersOnTop","CodeLength","DescriptionLength","CodeType","CodeAllowedLength", # NumberPeriodicity сюда НЕ входит: платформа считает его модификацией настроек нумерации и
"NumberType","NumberLength","NumberAllowedLength","NumberPeriodicity") # тогда требует объявить ещё и <Numerator/>, иначе /UpdateDBCfg падает — «отключать
# контролируемость свойства "Нумератор" недопустимо». Конфигуратор его не переносит
# (эталон заимствования документа: NumberType/NumberLength/NumberAllowedLength и всё).
# Загрузку это не ломает, ошибка вылезает только на обновлении конфигурации БД.
# FoldersOnTop сюда НЕ входит: платформа его у заимствованной оболочки не хранит — при
# загрузке молча выбрасывает (проверено раундтрипом: записали, выгрузили обратно, свойства
# нет). Конфигуратор его тоже не переносит. Остальные из списка сохраняются.
$propsToExtract = @("Hierarchical","CodeLength","DescriptionLength","CodeType","CodeAllowedLength",
"NumberType","NumberLength","NumberAllowedLength")
foreach ($pName in $propsToExtract) { foreach ($pName in $propsToExtract) {
$pNode = $propsNode.SelectSingleNode("md:${pName}", $srcNs) $pNode = $propsNode.SelectSingleNode("md:${pName}", $srcNs)
if ($pNode) { $extraProps[$pName] = $pNode.InnerText } if ($pNode) { $extraProps[$pName] = $pNode.InnerText }
@@ -1289,11 +1589,11 @@ function Resolve-SourceAttributes {
# --- 11d. Build adopted attribute XML --- # --- 11d. Build adopted attribute XML ---
function Build-AdoptedAttributeXml { function Build-AdoptedAttributeXml {
param([string]$name, [string]$sourceUuid, [string]$typeXml, [string]$indent) param([string]$name, [string]$sourceUuid, [string]$typeXml, [string]$indent, [string]$kind = "Attribute")
$newUuid = [guid]::NewGuid().ToString() $newUuid = [guid]::NewGuid().ToString()
$sb = New-Object System.Text.StringBuilder $sb = New-Object System.Text.StringBuilder
$sb.AppendLine("${indent}<Attribute uuid=`"${newUuid}`">") | Out-Null $sb.AppendLine("${indent}<${kind} uuid=`"${newUuid}`">") | Out-Null
$sb.AppendLine("${indent}`t<InternalInfo/>") | Out-Null $sb.AppendLine("${indent}`t<InternalInfo/>") | Out-Null
$sb.AppendLine("${indent}`t<Properties>") | Out-Null $sb.AppendLine("${indent}`t<Properties>") | Out-Null
$sb.AppendLine("${indent}`t`t<ObjectBelonging>Adopted</ObjectBelonging>") | Out-Null $sb.AppendLine("${indent}`t`t<ObjectBelonging>Adopted</ObjectBelonging>") | Out-Null
@@ -1302,7 +1602,7 @@ function Build-AdoptedAttributeXml {
$sb.AppendLine("${indent}`t`t<ExtendedConfigurationObject>${sourceUuid}</ExtendedConfigurationObject>") | Out-Null $sb.AppendLine("${indent}`t`t<ExtendedConfigurationObject>${sourceUuid}</ExtendedConfigurationObject>") | Out-Null
$sb.AppendLine("${indent}`t`t${typeXml}") | Out-Null $sb.AppendLine("${indent}`t`t${typeXml}") | Out-Null
$sb.AppendLine("${indent}`t</Properties>") | Out-Null $sb.AppendLine("${indent}`t</Properties>") | Out-Null
$sb.Append("${indent}</Attribute>") | Out-Null $sb.Append("${indent}</${kind}>") | Out-Null
return $sb.ToString() return $sb.ToString()
} }
@@ -1425,14 +1725,6 @@ function Merge-AttributesIntoObject {
$added = 0 $added = 0
foreach ($attr in $attrsToAdd) { foreach ($attr in $attrsToAdd) {
if ($existingNames.ContainsKey($attr.Name)) { continue } if ($existingNames.ContainsKey($attr.Name)) { continue }
$attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t"
# Expand self-closing ChildObjects if needed
if (-not $childObjs.HasChildNodes -or $childObjs.IsEmpty) {
$closeWs = $objDoc.CreateWhitespace("`r`n`t`t")
$childObjs.AppendChild($closeWs) | Out-Null
}
$added++ $added++
} }
@@ -1441,7 +1733,8 @@ function Merge-AttributesIntoObject {
$allAttrXml = "" $allAttrXml = ""
foreach ($attr in $attrsToAdd) { foreach ($attr in $attrsToAdd) {
if ($existingNames.ContainsKey($attr.Name)) { continue } if ($existingNames.ContainsKey($attr.Name)) { continue }
$allAttrXml += "`r`n" + (Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t") $kind = if ($attr.Kind) { $attr.Kind } else { "Attribute" }
$allAttrXml += "`r`n" + (Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t" $kind)
} }
# Save via text manipulation to avoid namespace issues with InnerXml # Save via text manipulation to avoid namespace issues with InnerXml
@@ -1459,8 +1752,9 @@ function Merge-AttributesIntoObject {
if ($text3.Length -gt 0 -and $text3[0] -eq [char]0xFEFF) { $text3 = $text3.Substring(1) } if ($text3.Length -gt 0 -and $text3[0] -eq [char]0xFEFF) { $text3 = $text3.Substring(1) }
$text3 = $text3.Replace('encoding="utf-8"', 'encoding="UTF-8"') $text3 = $text3.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Insert attributes before </ChildObjects> # Самозакрытый элемент раскрывается текстом, а не пробельным узлом в DOM: тот давал
$text3 = $text3 -replace '</ChildObjects>', "${allAttrXml}`r`n`t`t</ChildObjects>" # лишнюю строку с табуляцией перед первым <Attribute> (у Конфигуратора пустых строк нет).
$text3 = Insert-IntoOwnChildObjects $text3 $allAttrXml
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри # Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется), # CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
@@ -1494,7 +1788,16 @@ function Borrow-MainAttribute {
Write-Error "Source Form.xml not found: $srcFormXmlPath" Write-Error "Source Form.xml not found: $srcFormXmlPath"
exit 1 exit 1
} }
$dp = Collect-FormDataPaths $srcFormXmlPath # Имя основного реквизита исходной формы — корень путей, которые надо собрать
$dpDoc = New-Object System.Xml.XmlDocument
$dpDoc.PreserveWhitespace = $true
$dpDoc.Load($srcFormXmlPath)
$dpInfo = Get-MainAttributeInfo $dpDoc.DocumentElement '\s+xmlns(?::\w+)?="[^"]*"'
if (-not $dpInfo) {
Warn " У формы нет основного реквизита — заимствовать нечего"
return
}
$dp = Collect-FormDataPaths $srcFormXmlPath $dpInfo.Name
$firstLevelNames = $dp.FirstLevel $firstLevelNames = $dp.FirstLevel
$deepPaths = $dp.DeepPaths $deepPaths = $dp.DeepPaths
Info " Collected $($firstLevelNames.Count) first-level DataPath references, $($deepPaths.Count) deep paths" Info " Collected $($firstLevelNames.Count) first-level DataPath references, $($deepPaths.Count) deep paths"
@@ -1520,19 +1823,15 @@ function Borrow-MainAttribute {
$objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true))) $objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true)))
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow) # Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
$existingChildNames = @{} $existingChildNames = Get-OwnChildObjectNames $objFile
if ($objContent -match '(?s)<ChildObjects>(.*?)</ChildObjects>') {
foreach ($nm in [regex]::Matches($Matches[1], '<Name>(\w+)</Name>')) {
$existingChildNames[$nm.Groups[1].Value] = $true
}
}
$insertAttrs = @($srcAttrs | Where-Object { -not $existingChildNames.ContainsKey($_.Name) }) $insertAttrs = @($srcAttrs | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
$insertTS = @($srcTS | Where-Object { -not $existingChildNames.ContainsKey($_.Name) }) $insertTS = @($srcTS | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
# Generate full object XML with attributes and TS # Generate full object XML with attributes and TS
$contentSb = New-Object System.Text.StringBuilder $contentSb = New-Object System.Text.StringBuilder
foreach ($attr in $insertAttrs) { foreach ($attr in $insertAttrs) {
$attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t" $attrKind = if ($attr.Kind) { $attr.Kind } else { "Attribute" }
$attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t" $attrKind
$contentSb.AppendLine($attrXml) | Out-Null $contentSb.AppendLine($attrXml) | Out-Null
} }
foreach ($ts in $insertTS) { foreach ($ts in $insertTS) {
@@ -1557,17 +1856,9 @@ function Borrow-MainAttribute {
} }
} }
# Replace empty ChildObjects with adopted content # Добавить заимствованное содержимое в ChildObjects объекта (там уже может лежать <Form>)
if ($adoptedContent) { if ($adoptedContent) {
# Handle <ChildObjects/> (self-closing) $objContent = Insert-IntoOwnChildObjects $objContent "`r`n${adoptedContent}"
if ($objContent -match '<ChildObjects\s*/>') {
$objContent = $objContent -replace '<ChildObjects\s*/>', "<ChildObjects>`r`n${adoptedContent}`r`n`t`t</ChildObjects>"
}
# Handle <ChildObjects>...</ChildObjects> (may already have Form entry)
elseif ($objContent -match '(?s)<ChildObjects>(.*?)</ChildObjects>') {
$existingInner = $Matches[1]
$objContent = $objContent -replace '(?s)<ChildObjects>(.*?)</ChildObjects>', "<ChildObjects>${existingInner}`r`n${adoptedContent}`r`n`t`t</ChildObjects>"
}
} }
$encBom = New-Object System.Text.UTF8Encoding($true) $encBom = New-Object System.Text.UTF8Encoding($true)
@@ -1580,6 +1871,21 @@ function Borrow-MainAttribute {
foreach ($ts in $srcTS) { foreach ($ts in $srcTS) {
foreach ($tsa in $ts.Attributes) { $allTypeXmls += $tsa.TypeXml } foreach ($tsa in $ts.Attributes) { $allTypeXmls += $tsa.TypeXml }
} }
# Типы из <Columns> основного реквизита формы: колонку мы переносим (Borrow-Form), значит и её
# тип должен быть заимствован — иначе колонка ссылается на DefinedType/справочник, которого в
# расширении нет. Конфигуратор поступает так же (эталон: DefinedTypes/Артикул при заимствовании
# формы заказа поставщику).
$srcFormForCols = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $cfgDir $dirName) $objName) "Forms") $formName) "Ext/Form.xml"
if (Test-Path $srcFormForCols) {
$colsDoc = New-Object System.Xml.XmlDocument
$colsDoc.PreserveWhitespace = $true
$colsDoc.Load($srcFormForCols)
$colsInfo = Get-MainAttributeInfo $colsDoc.DocumentElement '\s+xmlns(?::\w+)?="[^"]*"'
if ($colsInfo) {
foreach ($m in [regex]::Matches($colsInfo.Xml, '(?s)<Columns>.*?</Columns>')) { $allTypeXmls += $m.Value }
}
}
$refTypes = Collect-ReferenceTypes $allTypeXmls $refTypes = Collect-ReferenceTypes $allTypeXmls
Info " Reference types to borrow: $($refTypes.Count)" Info " Reference types to borrow: $($refTypes.Count)"
@@ -1753,6 +2059,16 @@ function Build-BorrowedObjectXml {
$sb.AppendLine("`t`t`t$($sourceProps['__TypeXml'])") | Out-Null $sb.AppendLine("`t`t`t$($sourceProps['__TypeXml'])") | Out-Null
} }
# Свойства, от которых зависят стандартные поля (см. $script:typeGateProps / $script:typesWithOwners)
foreach ($gp in @($script:typeGateProps[$typeName])) {
if ($gp -and $sourceProps.ContainsKey($gp)) {
$sb.AppendLine("`t`t`t<${gp}>$($sourceProps[$gp])</${gp}>") | Out-Null
}
}
if ($sourceProps.ContainsKey("__OwnersXml")) {
$sb.AppendLine("`t`t`t$($sourceProps['__OwnersXml'])") | Out-Null
}
$sb.AppendLine("`t`t</Properties>") | Out-Null $sb.AppendLine("`t`t</Properties>") | Out-Null
# ChildObjects (for types that need it) # ChildObjects (for types that need it)
@@ -1920,6 +2236,47 @@ foreach ($item in $items) {
} }
} }
# --- 14b. Владельцы заимствованных справочников ---
# Ссылка в <Owners> должна вести на объект, который в расширении есть: иначе платформа падает при
# загрузке (проверено — access violation, не сообщение об ошибке). Конфигуратор владельца
# заимствует (эталон Issue66Example7_1: вместе со справочником перенесён и его ПВХ-владелец).
# Проход общий и повторяется, пока находятся новые: у владельца может быть свой владелец.
$ownerPass = 0
while ($true) {
$ownerPass++
if ($ownerPass -gt 10) { break }
$newOwners = @()
foreach ($shell in (Get-ChildItem -Path $extDir -Filter "*.xml" -Recurse -File)) {
$shellText = [System.IO.File]::ReadAllText($shell.FullName)
if ($shellText -notmatch '<Owners>') { continue }
foreach ($om in [regex]::Matches($shellText, '<xr:Item[^>]*>(\w+)\.(\w+)</xr:Item>')) {
$oType = $om.Groups[1].Value; $oName = $om.Groups[2].Value
if (-not $childTypeDirMap.ContainsKey($oType)) { continue }
if (Test-ObjectBorrowed $oType $oName) { continue }
if ($newOwners | Where-Object { $_.T -eq $oType -and $_.N -eq $oName }) { continue }
$newOwners += @{ T = $oType; N = $oName }
}
}
if ($newOwners.Count -eq 0) { break }
foreach ($ow in $newOwners) {
$owSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$ow.T]) "$($ow.N).xml"
if (-not (Test-Path $owSrcFile)) {
Warn " Владелец $($ow.T).$($ow.N) не найден в источнике — ссылка останется висячей"
continue
}
$owSrc = Read-SourceObject $ow.T $ow.N
$owXml = Build-BorrowedObjectXml $ow.T $ow.N $owSrc.Uuid $owSrc.Properties
$owDir = Join-Path $extDir $childTypeDirMap[$ow.T]
if (-not (Test-Path $owDir)) { New-Item -ItemType Directory -Path $owDir -Force | Out-Null }
$owFile = Join-Path $owDir "$($ow.N).xml"
$owEnc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($owFile, $owXml, $owEnc)
Add-ToChildObjects $ow.T $ow.N
$script:borrowedFiles += $owFile
Info " Auto-borrowed owner: $($ow.T).$($ow.N)"
}
}
# --- 15. Save modified Configuration.xml --- # --- 15. Save modified Configuration.xml ---
$settings = New-Object System.Xml.XmlWriterSettings $settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = New-Object System.Text.UTF8Encoding($true) $settings.Encoding = New-Object System.Text.UTF8Encoding($true)
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cfe-borrow v1.20 — Borrow objects from configuration into extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы) # cfe-borrow v1.31 — Borrow objects from configuration into extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -39,18 +39,35 @@ V8_NS = "http://v8.1c.ru/8.1/data/core"
# Form data-binding tags (value = attribute path). A binding survives only if its root # Form data-binding tags (value = attribute path). A binding survives only if its root
# attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the # attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the
# platform rejects the form with "Неверный путь к данным" on load. # platform rejects the form with "Неверный путь к данным" on load.
FORM_BINDING_DATA_TAGS = ["DataPath", "TitleDataPath", "FooterDataPath", "HeaderDataPath", "MultipleValueDataPath", "MultipleValuePresentDataPath"] # RowPictureDataPath тоже путь к данным («Объект.Товары.РасхождениеЗаказ», «Список.DefaultPicture»),
# а не индекс картинки: эталон Конфигуратора сохраняет его с заимствованным основным реквизитом
# и выбрасывает без него — то же правило, что у остальных путей.
FORM_BINDING_DATA_TAGS = ["DataPath", "TitleDataPath", "FooterDataPath", "HeaderDataPath", "MultipleValueDataPath", "MultipleValuePresentDataPath", "RowPictureDataPath"]
# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton. # Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton.
FORM_BINDING_PICTURE_TAGS = ["RowPictureDataPath", "MultipleValuePictureDataPath"] FORM_BINDING_PICTURE_TAGS = ["MultipleValuePictureDataPath"]
# id основного реквизита в заимствованной форме — как у Конфигуратора
MAIN_ATTR_ID = "1000001"
# Виды дочерних объектов, которые заимствуются в оболочку поимённо (табличные части — отдельно)
CHILD_OBJECT_KINDS = ("Attribute", "Dimension", "Resource", "AddressingAttribute")
# Прямые дети <Form>, которые в заимствованную форму не переносятся.
# Структурные секции: AutoCommandBar и ChildItems забираются отдельно, остальные выбрасываются целиком.
FORM_STRUCTURAL_SECTIONS = ("Events", "Attributes", "Commands", "Parameters", "CommandInterface")
# Свойства формы, значение которых — имя реквизита формы (реквизиты не заимствуются, ссылка повиснет).
FORM_ATTRIBUTE_REF_PROPS = ("ReportResult", "DetailsData", "VariantAppearance", "GroupList")
def strip_form_bindings(xml, keep_objekt): def strip_form_bindings(xml, main_attr_name):
"""Strip data-binding tags whose root attribute isn't borrowed. """Strip data-binding tags whose root attribute isn't borrowed.
keep_objekt=True (BorrowMainAttribute): keep Объект.* data bindings, strip the rest. main_attr_name задан (BorrowMainAttribute): оставить привязки от его имени, остальные снять.
keep_objekt=False (default skeleton): strip all bindings. Picture-path tags are always stripped.""" Пусто (скелет без основного реквизита): снять все. Картиночные пути снимаются всегда."""
for tag in FORM_BINDING_DATA_TAGS: for tag in FORM_BINDING_DATA_TAGS:
if keep_objekt: if main_attr_name:
xml = re.sub(rf'\s*<{tag}>(?!Объект\.)[^<]*</{tag}>', '', xml) # Оставить и «Список.Поле», и путь ровно на сам реквизит («Список» у таблицы формы)
root = re.escape(main_attr_name)
xml = re.sub(rf'\s*<{tag}>(?!{root}(\.|<))[^<]*</{tag}>', '', xml)
else: else:
xml = re.sub(rf'\s*<{tag}>[^<]*</{tag}>', '', xml) xml = re.sub(rf'\s*<{tag}>[^<]*</{tag}>', '', xml)
for tag in FORM_BINDING_PICTURE_TAGS: for tag in FORM_BINDING_PICTURE_TAGS:
@@ -58,6 +75,124 @@ def strip_form_bindings(xml, keep_objekt):
return xml return xml
DROPPED_LINKS = []
def rewrite_choice_parameter_links(xml, attr_uuids, form_attr_ids, main_attr_name, main_attr_borrowed):
"""Ссылки параметров выбора (<ChoiceParameterLinks>/<xr:Link>) — привязка особого рода: путь лежит
в <xr:DataPath> и обычным стриппингом не снимается. Текстовое имя в расширении разрешается только
если его корень объявлен в <Attributes> самой заимствованной формы; иначе платформа отвергает
загрузку «Неверный путь к полю - X». Реквизиты формы не заимствуются никогда, поэтому ссылка на
них разрешима только через id: Конфигуратор подставляет id реквизита ИСХОДНОЙ формы (эталоны
Issue66Example4/5/6, JR2433, JR2976, JR49904 совпадение на шести расширениях). Именно id
исходной, а не заимствованной: при заимствовании реквизиты перенумеровываются в 1000000+, а
ссылка продолжает указывать в нумерацию базовой формы.
Путь на основной реквизит («Объект.X») при заимствованном основном реквизите разрешается текстом
и остаётся читаемым; без заимствования переводится в «<id>/0:<uuid реквизита объекта>».
Реквизит, которого в источнике нет, недоступен и по uuid: такую связь вырезаем целиком."""
if '<ChoiceParameterLinks>' not in xml:
return xml
main_pat = re.escape(main_attr_name) if main_attr_name else None
main_id = form_attr_ids.get(main_attr_name, "1") if main_attr_name else "1"
def repl(m):
link = m.group(0)
dp = re.search(r'<xr:DataPath[^>]*>([^<]+)</xr:DataPath>', link)
if not dp:
return link
path = dp.group(1)
# Путь на основной реквизит формы
if main_pat:
mm = re.match('^' + main_pat + r'\.(.+)$', path)
if mm:
attr_name = mm.group(1)
if main_attr_borrowed:
# Реквизит объекта разрешается текстом и остаётся читаемым. Стандартное поле
# («Объект.Owner», «Объект.Date») — нет: платформа отвергает «Неверный путь к данным».
# Конфигуратор в этом случае оставляет ссылку на сам реквизит (эталон Issue66Example7_1).
if attr_name in attr_uuids:
return link
return re.sub(r'(<xr:DataPath[^>]*>)[^<]+(</xr:DataPath>)',
lambda x: f"{x.group(1)}{main_id}{x.group(2)}", link)
if attr_name in attr_uuids:
return re.sub(r'(<xr:DataPath[^>]*>)[^<]+(</xr:DataPath>)',
lambda x: f"{x.group(1)}{main_id}/0:{attr_uuids[attr_name]}{x.group(2)}", link)
return ''
# Односегментный путь на реквизит формы — только по id исходной формы
if '.' not in path and path in form_attr_ids:
return re.sub(r'(<xr:DataPath[^>]*>)[^<]+(</xr:DataPath>)',
lambda x: f"{x.group(1)}{form_attr_ids[path]}{x.group(2)}", link)
# Уже непрозрачный путь (форма-источник сама из расширения) — не трогаем
if re.match(r'^\d', path):
return link
# С заимствованным основным реквизитом текстовый путь разрешается: элементы формы на месте,
# а их данные доступны через основной реквизит. Конфигуратор такие пути и оставляет текстом
# (эталон Issue66Example7_1: «Items.Товары.CurrentData.Характеристика» перенесён как есть).
if main_attr_borrowed:
return link
# Прочее текстом не разрешается: платформа отвергает загрузку «Неверный путь к полю».
# Сюда попадают «Items.<Элемент>.CurrentData.<Поле>» — их кодировка непрозрачна и по
# имеющимся эталонам не воспроизводима. Связь параметров выбора — удобство подбора, а не
# данные: без неё форма заимствуется и работает, с ней — не грузится вовсе.
DROPPED_LINKS.append(path)
return ''
xml = re.sub(r'\s*<xr:Link>.*?</xr:Link>', repl, xml, flags=re.DOTALL)
# Опустевший контейнер платформе не нужен
xml = re.sub(r'\s*<ChoiceParameterLinks>\s*</ChoiceParameterLinks>', '', xml, flags=re.DOTALL)
return xml
def get_own_child_object_names(obj_file):
"""Имена ПРЯМЫХ детей собственного <ChildObjects> объекта — для дедупа при повторном
заимствовании. Текстом это не снять: regex «первый <ChildObjects> до первого </ChildObjects>»
у объекта с табличными частями обрывается на закрытии первой ТЧ, забирает имена её колонок и
теряет то, что идёт после неё."""
names = set()
try:
tree = etree.parse(obj_file)
except Exception:
return names
root = tree.getroot()
obj_el = next((c for c in root if isinstance(c.tag, str)), None)
if obj_el is None:
return names
child_objs = next((c for c in obj_el if isinstance(c.tag, str) and localname(c) == "ChildObjects"), None)
if child_objs is None:
return names
for child in child_objs:
if not isinstance(child.tag, str):
continue
props = next((p for p in child if isinstance(p.tag, str) and localname(p) == "Properties"), None)
if props is None:
continue
nm = next((n for n in props if isinstance(n.tag, str) and localname(n) == "Name"), None)
if nm is not None and nm.text:
names.add(nm.text.strip())
return names
def insert_into_own_child_objects(text, content):
"""Вставка в СОБСТВЕННЫЙ <ChildObjects> объекта. Свой контейнер закрывается в файле последним:
объект в файле один, а вложенные <ChildObjects> табличных частей закрываются раньше. Замена по
всем вхождениям раскидывала реквизиты по каждой ТЧ ps1 рвал XML, py прятал ТЧ внутрь ТЧ."""
close_idx = text.rfind("</ChildObjects>")
if close_idx >= 0:
return text[:close_idx] + content + "\r\n\t\t" + text[close_idx:]
# Своего закрывающего тега нет — значит контейнер самозакрытый (детей у него нет, вложенных тоже)
self_matches = list(re.finditer(r'<ChildObjects\s*/>', text))
if not self_matches:
return text
m = self_matches[-1]
return text[:m.start()] + f"<ChildObjects>{content}\r\n\t\t</ChildObjects>" + text[m.end():]
def decode_numeric_entities(s): def decode_numeric_entities(s):
"""lxml emits numeric character refs (&#xNNNN;) for non-ASCII in some self-closed """lxml emits numeric character refs (&#xNNNN;) for non-ASCII in some self-closed
elements where the PowerShell port writes literal characters. Normalize numeric refs elements where the PowerShell port writes literal characters. Normalize numeric refs
@@ -202,6 +337,7 @@ GENERATED_TYPES = {
], ],
"AccountingRegister": [ "AccountingRegister": [
{"prefix": "AccountingRegisterRecord", "category": "Record"}, {"prefix": "AccountingRegisterRecord", "category": "Record"},
{"prefix": "AccountingRegisterExtDimensions", "category": "ExtDimensions"},
{"prefix": "AccountingRegisterManager", "category": "Manager"}, {"prefix": "AccountingRegisterManager", "category": "Manager"},
{"prefix": "AccountingRegisterSelection", "category": "Selection"}, {"prefix": "AccountingRegisterSelection", "category": "Selection"},
{"prefix": "AccountingRegisterList", "category": "List"}, {"prefix": "AccountingRegisterList", "category": "List"},
@@ -215,6 +351,7 @@ GENERATED_TYPES = {
{"prefix": "CalculationRegisterList", "category": "List"}, {"prefix": "CalculationRegisterList", "category": "List"},
{"prefix": "CalculationRegisterRecordSet", "category": "RecordSet"}, {"prefix": "CalculationRegisterRecordSet", "category": "RecordSet"},
{"prefix": "CalculationRegisterRecordKey", "category": "RecordKey"}, {"prefix": "CalculationRegisterRecordKey", "category": "RecordKey"},
{"prefix": "RecalculationsManager", "category": "Recalcs"},
], ],
"ChartOfAccounts": [ "ChartOfAccounts": [
{"prefix": "ChartOfAccountsObject", "category": "Object"}, {"prefix": "ChartOfAccountsObject", "category": "Object"},
@@ -222,12 +359,15 @@ GENERATED_TYPES = {
{"prefix": "ChartOfAccountsSelection", "category": "Selection"}, {"prefix": "ChartOfAccountsSelection", "category": "Selection"},
{"prefix": "ChartOfAccountsList", "category": "List"}, {"prefix": "ChartOfAccountsList", "category": "List"},
{"prefix": "ChartOfAccountsManager", "category": "Manager"}, {"prefix": "ChartOfAccountsManager", "category": "Manager"},
{"prefix": "ChartOfAccountsExtDimensionTypes", "category": "ExtDimensionTypes"},
{"prefix": "ChartOfAccountsExtDimensionTypesRow", "category": "ExtDimensionTypesRow"},
], ],
"ChartOfCharacteristicTypes": [ "ChartOfCharacteristicTypes": [
{"prefix": "ChartOfCharacteristicTypesObject", "category": "Object"}, {"prefix": "ChartOfCharacteristicTypesObject", "category": "Object"},
{"prefix": "ChartOfCharacteristicTypesRef", "category": "Ref"}, {"prefix": "ChartOfCharacteristicTypesRef", "category": "Ref"},
{"prefix": "ChartOfCharacteristicTypesSelection", "category": "Selection"}, {"prefix": "ChartOfCharacteristicTypesSelection", "category": "Selection"},
{"prefix": "ChartOfCharacteristicTypesList", "category": "List"}, {"prefix": "ChartOfCharacteristicTypesList", "category": "List"},
{"prefix": "Characteristic", "category": "Characteristic"},
{"prefix": "ChartOfCharacteristicTypesManager", "category": "Manager"}, {"prefix": "ChartOfCharacteristicTypesManager", "category": "Manager"},
], ],
"ChartOfCalculationTypes": [ "ChartOfCalculationTypes": [
@@ -237,8 +377,11 @@ GENERATED_TYPES = {
{"prefix": "ChartOfCalculationTypesList", "category": "List"}, {"prefix": "ChartOfCalculationTypesList", "category": "List"},
{"prefix": "ChartOfCalculationTypesManager", "category": "Manager"}, {"prefix": "ChartOfCalculationTypesManager", "category": "Manager"},
{"prefix": "DisplacingCalculationTypes", "category": "DisplacingCalculationTypes"}, {"prefix": "DisplacingCalculationTypes", "category": "DisplacingCalculationTypes"},
{"prefix": "DisplacingCalculationTypesRow", "category": "DisplacingCalculationTypesRow"},
{"prefix": "BaseCalculationTypes", "category": "BaseCalculationTypes"}, {"prefix": "BaseCalculationTypes", "category": "BaseCalculationTypes"},
{"prefix": "BaseCalculationTypesRow", "category": "BaseCalculationTypesRow"},
{"prefix": "LeadingCalculationTypes", "category": "LeadingCalculationTypes"}, {"prefix": "LeadingCalculationTypes", "category": "LeadingCalculationTypes"},
{"prefix": "LeadingCalculationTypesRow", "category": "LeadingCalculationTypesRow"},
], ],
"BusinessProcess": [ "BusinessProcess": [
{"prefix": "BusinessProcessObject", "category": "Object"}, {"prefix": "BusinessProcessObject", "category": "Object"},
@@ -246,6 +389,7 @@ GENERATED_TYPES = {
{"prefix": "BusinessProcessSelection", "category": "Selection"}, {"prefix": "BusinessProcessSelection", "category": "Selection"},
{"prefix": "BusinessProcessList", "category": "List"}, {"prefix": "BusinessProcessList", "category": "List"},
{"prefix": "BusinessProcessManager", "category": "Manager"}, {"prefix": "BusinessProcessManager", "category": "Manager"},
{"prefix": "BusinessProcessRoutePointRef", "category": "RoutePointRef"},
], ],
"Task": [ "Task": [
{"prefix": "TaskObject", "category": "Object"}, {"prefix": "TaskObject", "category": "Object"},
@@ -311,6 +455,16 @@ TYPES_WITH_CHILD_OBJECTS = [
COMMON_MODULE_PROPS = ["Global", "ClientManagedApplication", "Server", "ExternalConnection", "ClientOrdinaryApplication", "ServerCall"] COMMON_MODULE_PROPS = ["Global", "ClientManagedApplication", "Server", "ExternalConnection", "ClientOrdinaryApplication", "ServerCall"]
# Свойства объекта, от которых зависит существование стандартного поля: без них платформа
# отвергает загрузку — «Неверный путь к данным». Конфигуратор переносит ровно их (эталоны
# Issue66Example7_1 и Issue66Example2). Проверено сплошным прогоном по типам: у регистра
# сведений без InformationRegisterPeriodicity не разрешается «Запись.Period».
TYPE_GATE_PROPS = {
"InformationRegister": ["InformationRegisterPeriodicity", "WriteMode"],
}
# Владельцы справочника — список <xr:Item>, а не скаляр: переносится фрагментом, как __TypeXml
TYPES_WITH_OWNERS = ("Catalog", "ChartOfCharacteristicTypes")
# Standard system fields to skip when collecting DataPath references # Standard system fields to skip when collecting DataPath references
STANDARD_FIELDS = [ STANDARD_FIELDS = [
"Code", "Description", "Ref", "Parent", "DeletionMark", "Code", "Description", "Ref", "Parent", "DeletionMark",
@@ -582,6 +736,45 @@ def main():
borrowed_files = [] borrowed_files = []
# --- Helper functions --- # --- Helper functions ---
def get_source_attribute_uuids(type_name, obj_name):
"""Имена реквизитов исходного объекта → uuid. Нужны для непрозрачной формы пути в ссылках
параметров выбора (см. rewrite_choice_parameter_links)."""
result = {}
dir_name = CHILD_TYPE_DIR_MAP.get(type_name)
if not dir_name:
return result
src_file = os.path.join(cfg_dir, dir_name, f"{obj_name}.xml")
if not os.path.isfile(src_file):
return result
tree = etree.parse(src_file, etree.XMLParser(remove_blank_text=True))
obj_el = None
for c in tree.getroot():
if isinstance(c.tag, str):
obj_el = c
break
if obj_el is None:
return result
for child in obj_el:
if not isinstance(child.tag, str) or localname(child) != "ChildObjects":
continue
for sub in child:
if not isinstance(sub.tag, str) or localname(sub) not in ("Attribute", "TabularSection"):
continue
uuid_val = sub.get("uuid")
name_val = None
for props in sub:
if isinstance(props.tag, str) and localname(props) == "Properties":
for prop in props:
if isinstance(prop.tag, str) and localname(prop) == "Name":
name_val = (prop.text or "").strip()
break
break
if uuid_val and name_val:
result[name_val] = uuid_val
break
return result
def read_source_object(type_name, obj_name): def read_source_object(type_name, obj_name):
dir_name = CHILD_TYPE_DIR_MAP.get(type_name) dir_name = CHILD_TYPE_DIR_MAP.get(type_name)
if not dir_name: if not dir_name:
@@ -625,6 +818,17 @@ def main():
if type_node is not None: if type_node is not None:
type_xml = etree.tostring(type_node, encoding="unicode") type_xml = etree.tostring(type_node, encoding="unicode")
src_props["__TypeXml"] = re.sub(r'\s+xmlns(?::\w+)?="[^"]*"', '', type_xml) src_props["__TypeXml"] = re.sub(r'\s+xmlns(?::\w+)?="[^"]*"', '', type_xml)
# Владельцы: стандартное поле «Owner» появляется у справочника, только если задан Owners
if type_name in TYPES_WITH_OWNERS:
owners_node = props_node.find(f"{{{MD_NS}}}Owners")
if owners_node is not None and len(owners_node):
owners_xml = etree.tostring(owners_node, encoding="unicode")
src_props["__OwnersXml"] = re.sub(r'\s+xmlns(?::\w+)?="[^"]*"', '', owners_xml)
# Скалярные свойства, включающие стандартные поля своего типа
for gp in TYPE_GATE_PROPS.get(type_name, []):
gp_node = props_node.find(f"{{{MD_NS}}}{gp}")
if gp_node is not None:
src_props[gp] = (gp_node.text or "").strip()
# Whether the platform emits <ChildObjects> for this type — the source object is the ground truth # Whether the platform emits <ChildObjects> for this type — the source object is the ground truth
src_props["__HasChildObjects"] = src_el.find(f"{{{MD_NS}}}ChildObjects") is not None src_props["__HasChildObjects"] = src_el.find(f"{{{MD_NS}}}ChildObjects") is not None
@@ -703,6 +907,13 @@ def main():
if type_name == "DefinedType" and "__TypeXml" in source_props: if type_name == "DefinedType" and "__TypeXml" in source_props:
lines.append(f"\t\t\t{source_props['__TypeXml']}") lines.append(f"\t\t\t{source_props['__TypeXml']}")
# Свойства, от которых зависят стандартные поля (см. TYPE_GATE_PROPS / TYPES_WITH_OWNERS)
for gp in TYPE_GATE_PROPS.get(type_name, []):
if gp in source_props:
lines.append(f"\t\t\t<{gp}>{source_props[gp]}</{gp}>")
if "__OwnersXml" in source_props:
lines.append(f"\t\t\t{source_props['__OwnersXml']}")
lines.append("\t\t</Properties>") lines.append("\t\t</Properties>")
if source_props.get("__HasChildObjects") or type_name in TYPES_WITH_CHILD_OBJECTS: if source_props.get("__HasChildObjects") or type_name in TYPES_WITH_CHILD_OBJECTS:
@@ -806,8 +1017,59 @@ def main():
save_xml_bom(obj_tree, obj_file) save_xml_bom(obj_tree, obj_file)
info(f" Registered form in: {obj_file}") info(f" Registered form in: {obj_file}")
# --- 11b1. Основной реквизит исходной формы ---
# Переносится ЦЕЛИКОМ, а не собирается из констант: имя, тип и состав детей зависят от вида формы.
# У формы объекта это «Объект»/<Тип>Object + SavedData/UseAlways/Columns, у формы списка —
# «Список»/DynamicList + Settings, у формы записи регистра — «Запись»/RecordManager + SavedData.
# Синтез фиксированного набора давал для необъектных форм «Исключение XDTO» при загрузке.
# Конфигуратор меняет у скопированного реквизита только id (эталоны Issue64UtB, Issue66Example2).
def get_form_attribute_ids(form_el):
"""Имена реквизитов ИСХОДНОЙ формы → их id. Ссылки параметров выбора адресуют реквизит формы
именно по id базовой формы (см. rewrite_choice_parameter_links)."""
result = {}
for child in form_el:
if not isinstance(child.tag, str) or localname(child) != "Attributes":
continue
for a in child:
if not isinstance(a.tag, str) or localname(a) != "Attribute":
continue
nm, aid = a.get("name"), a.get("id")
if nm and aid:
result[nm] = aid
break
return result
def get_main_attribute_info(form_el, ns_strip_pattern):
main_attr = None
for child in form_el:
if not isinstance(child.tag, str) or localname(child) != "Attributes":
continue
for attr in child:
if not isinstance(attr.tag, str) or localname(attr) != "Attribute":
continue
for sub in attr:
if isinstance(sub.tag, str) and localname(sub) == "MainAttribute" and (sub.text or "").strip() == "true":
main_attr = attr
break
if main_attr is not None:
break
break
if main_attr is None:
return None
# with_tail=False: хвостовой пробельный узел — часть родителя, а не секции; иначе в
# вывод попадают пустые строки, которых нет у PS (OuterXml хвост не включает).
xml = decode_numeric_entities(etree.tostring(main_attr, encoding="unicode", with_tail=False))
xml = ns_strip_pattern.sub("", xml)
# id заменяется только в открывающем теге самого реквизита — у вложенных элементов свои
xml = re.sub(r'^(<Attribute\s[^>]*?)id="[^"]*"', lambda m: m.group(1) + f'id="{MAIN_ATTR_ID}"', xml)
return {"Name": main_attr.get("name"), "Xml": xml}
# --- 11b. Collect DataPath references from source Form.xml --- # --- 11b. Collect DataPath references from source Form.xml ---
def collect_form_data_paths(form_xml_path): def collect_form_data_paths(form_xml_path, main_attr_name):
# Корень путей — имя основного реквизита формы: «Объект» у формы объекта, «Список» у формы
# списка, «Запись» у формы записи регистра. Зашитый «Объект» не находил ничего у необъектных
# форм, и в оболочку не заимствовалось ни одного дочернего объекта.
root = re.escape(main_attr_name)
with open(form_xml_path, "r", encoding="utf-8-sig") as fh: with open(form_xml_path, "r", encoding="utf-8-sig") as fh:
content = fh.read() content = fh.read()
@@ -817,7 +1079,7 @@ def main():
# Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*) # Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*)
# for Объект.* references — picture-path tags carry picture indices, not data attributes. # for Объект.* references — picture-path tags carry picture indices, not data attributes.
for tag in FORM_BINDING_DATA_TAGS: for tag in FORM_BINDING_DATA_TAGS:
for m in re.finditer(r'<' + tag + r'>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</' + tag + r'>', content): for m in re.finditer(r'<' + tag + r'>[^<]*\b' + root + r'\.(\w+(?:\.\w+)*)</' + tag + r'>', content):
path = m.group(1) path = m.group(1)
segments = path.split(".") segments = path.split(".")
seg0 = segments[0] seg0 = segments[0]
@@ -833,7 +1095,7 @@ def main():
# Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance # Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance
# fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too. # fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too.
for m in re.finditer(r'<Field>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</Field>', content): for m in re.finditer(r'<Field>[^<]*\b' + root + r'\.(\w+(?:\.\w+)*)</Field>', content):
path = m.group(1) path = m.group(1)
segments = path.split(".") segments = path.split(".")
seg0 = segments[0] seg0 = segments[0]
@@ -847,6 +1109,27 @@ def main():
seg2 = segments[2] if len(segments) >= 3 else None seg2 = segments[2] if len(segments) >= 3 else None
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2}) deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
# Also scan <AdditionalColumns table="Объект.X"> — доп. колонки табличной части, объявленные в
# самой форме (напр. Объект.Товары.Артикул). Такая ТЧ может больше нигде на форме не встречаться,
# и без её заимствования платформа отвергает форму: «Неверный путь к данным».
for m in re.finditer(r'<AdditionalColumns table="' + root + r'\.(\w+)"', content):
seg0 = m.group(1)
if seg0 in STANDARD_FIELDS:
continue
first_level[seg0] = True
# Текст запроса динамического списка — такое же место ссылки на реквизиты объекта, как DataPath.
# Конфигуратор заимствует всё, что упомянуто в запросе: на эталоне Issue66Example2 это 21 из 27
# дочерних объектов, совпадение с ним точное в обе стороны. У списка без ручного запроса
# (<QueryText> нет) заимствуется только видимое на форме — эталон Issue66Example3.
# Разбирать язык запросов не нужно: имена-кандидаты отфильтрует resolve_source_attributes по
# реальному составу объекта, поэтому лишние слова из запроса безвредны.
for qm in re.finditer(r'(?s)<QueryText>(.*?)</QueryText>', content):
for w in re.finditer(r'\w+', qm.group(1)):
if w.group(0) in STANDARD_FIELDS:
continue
first_level[w.group(0)] = True
# Deduplicate deep paths # Deduplicate deep paths
seen = set() seen = set()
unique_deep = [] unique_deep = []
@@ -894,7 +1177,11 @@ def main():
continue continue
ln = localname(child) ln = localname(child)
if ln == "Attribute": # Реквизит объекта, измерение и ресурс регистра — один и тот же вид дочернего объекта с
# точки зрения заимствования, различается только имя элемента. Конфигуратор переносит их
# своим видом (эталон Issue66Example2: у регистра <Dimension> x3 и <Resource>), поэтому вид
# запоминается и выпускается как есть — иначе измерение уехало бы в файл как <Attribute>.
if ln in CHILD_OBJECT_KINDS:
name_node = child.find(f"{{{MD_NS}}}Properties/{{{MD_NS}}}Name") name_node = child.find(f"{{{MD_NS}}}Properties/{{{MD_NS}}}Name")
if name_node is None: if name_node is None:
continue continue
@@ -909,7 +1196,7 @@ def main():
type_xml = etree.tostring(type_node, encoding="unicode") type_xml = etree.tostring(type_node, encoding="unicode")
type_xml = ns_strip.sub("", type_xml) type_xml = ns_strip.sub("", type_xml)
attrs.append({"Name": attr_name, "Uuid": attr_uuid, "TypeXml": type_xml}) attrs.append({"Name": attr_name, "Uuid": attr_uuid, "TypeXml": type_xml, "Kind": ln})
elif ln == "TabularSection": elif ln == "TabularSection":
name_node = child.find(f"{{{MD_NS}}}Properties/{{{MD_NS}}}Name") name_node = child.find(f"{{{MD_NS}}}Properties/{{{MD_NS}}}Name")
@@ -969,10 +1256,18 @@ def main():
extra_props = {} extra_props = {}
props_node = src_el.find(f"{{{MD_NS}}}Properties") props_node = src_el.find(f"{{{MD_NS}}}Properties")
if props_node is not None: if props_node is not None:
# NumberPeriodicity сюда НЕ входит: платформа считает его модификацией настроек нумерации и
# тогда требует объявить ещё и <Numerator/>, иначе /UpdateDBCfg падает — «отключать
# контролируемость свойства "Нумератор" недопустимо». Конфигуратор его не переносит
# (эталон заимствования документа: NumberType/NumberLength/NumberAllowedLength и всё).
# Загрузку это не ломает, ошибка вылезает только на обновлении конфигурации БД.
# FoldersOnTop сюда НЕ входит: платформа его у заимствованной оболочки не хранит — при
# загрузке молча выбрасывает (проверено раундтрипом: записали, выгрузили обратно, свойства
# нет). Конфигуратор его тоже не переносит. Остальные из списка сохраняются.
props_to_extract = [ props_to_extract = [
"Hierarchical", "FoldersOnTop", "CodeLength", "DescriptionLength", "Hierarchical", "CodeLength", "DescriptionLength",
"CodeType", "CodeAllowedLength", "NumberType", "NumberLength", "CodeType", "CodeAllowedLength", "NumberType", "NumberLength",
"NumberAllowedLength", "NumberPeriodicity", "NumberAllowedLength",
] ]
for p_name in props_to_extract: for p_name in props_to_extract:
p_node = props_node.find(f"{{{MD_NS}}}{p_name}") p_node = props_node.find(f"{{{MD_NS}}}{p_name}")
@@ -982,10 +1277,10 @@ def main():
return {"Attributes": attrs, "TabularSections": tab_sections, "ExtraProps": extra_props} return {"Attributes": attrs, "TabularSections": tab_sections, "ExtraProps": extra_props}
# --- 11d. Build adopted attribute XML --- # --- 11d. Build adopted attribute XML ---
def build_adopted_attribute_xml(name, source_uuid, type_xml, indent): def build_adopted_attribute_xml(name, source_uuid, type_xml, indent, kind="Attribute"):
new_uuid_val = new_guid() new_uuid_val = new_guid()
lines = [ lines = [
f'{indent}<Attribute uuid="{new_uuid_val}">', f'{indent}<{kind} uuid="{new_uuid_val}">',
f'{indent}\t<InternalInfo/>', f'{indent}\t<InternalInfo/>',
f'{indent}\t<Properties>', f'{indent}\t<Properties>',
f'{indent}\t\t<ObjectBelonging>Adopted</ObjectBelonging>', f'{indent}\t\t<ObjectBelonging>Adopted</ObjectBelonging>',
@@ -994,7 +1289,7 @@ def main():
f'{indent}\t\t<ExtendedConfigurationObject>{source_uuid}</ExtendedConfigurationObject>', f'{indent}\t\t<ExtendedConfigurationObject>{source_uuid}</ExtendedConfigurationObject>',
f'{indent}\t\t{type_xml}', f'{indent}\t\t{type_xml}',
f'{indent}\t</Properties>', f'{indent}\t</Properties>',
f'{indent}</Attribute>', f'{indent}</{kind}>',
] ]
return "\n".join(lines) return "\n".join(lines)
@@ -1069,25 +1364,19 @@ def main():
with open(obj_file, "r", encoding="utf-8-sig", newline="") as fh: with open(obj_file, "r", encoding="utf-8-sig", newline="") as fh:
obj_content = fh.read() obj_content = fh.read()
# Collect existing attribute names for dedup (text-based) # Collect existing names for dedup — только прямые дети своего ChildObjects
existing_names = set() existing_names = get_own_child_object_names(obj_file)
for m in re.finditer(r'<Name>(\w+)</Name>', obj_content):
existing_names.add(m.group(1))
all_attr_xml = "" all_attr_xml = ""
added = 0 added = 0
for attr in attrs_to_add: for attr in attrs_to_add:
if attr["Name"] in existing_names: if attr["Name"] in existing_names:
continue continue
all_attr_xml += "\r\n" + build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t") all_attr_xml += "\r\n" + build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t", attr.get("Kind", "Attribute"))
added += 1 added += 1
if added > 0: if added > 0:
# Insert attributes — handle both <ChildObjects/> and <ChildObjects>...</ChildObjects> obj_content = insert_into_own_child_objects(obj_content, all_attr_xml)
if re.search(r'<ChildObjects\s*/>', obj_content):
obj_content = re.sub(r'<ChildObjects\s*/>', f"<ChildObjects>{all_attr_xml}\r\n\t\t</ChildObjects>", obj_content)
else:
obj_content = obj_content.replace("</ChildObjects>", f"{all_attr_xml}\r\n\t\t</ChildObjects>")
write_utf8_bom(obj_file, obj_content) write_utf8_bom(obj_file, obj_content)
info(f" Merged {added} attribute(s) into: {obj_file}") info(f" Merged {added} attribute(s) into: {obj_file}")
@@ -1104,7 +1393,13 @@ def main():
if not os.path.isfile(src_form_xml_path): if not os.path.isfile(src_form_xml_path):
print(f"Source Form.xml not found: {src_form_xml_path}", file=sys.stderr) print(f"Source Form.xml not found: {src_form_xml_path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
dp = collect_form_data_paths(src_form_xml_path) # Имя основного реквизита исходной формы — корень путей, которые надо собрать
dp_ns_strip = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
dp_info = get_main_attribute_info(etree.parse(src_form_xml_path).getroot(), dp_ns_strip)
if dp_info is None:
warn(" У формы нет основного реквизита — заимствовать нечего")
return
dp = collect_form_data_paths(src_form_xml_path, dp_info["Name"])
first_level_names = dp["FirstLevel"] first_level_names = dp["FirstLevel"]
deep_paths = dp["DeepPaths"] deep_paths = dp["DeepPaths"]
info(f" Collected {len(first_level_names)} first-level DataPath references, {len(deep_paths)} deep paths") info(f" Collected {len(first_level_names)} first-level DataPath references, {len(deep_paths)} deep paths")
@@ -1131,18 +1426,14 @@ def main():
obj_content = fh.read() obj_content = fh.read()
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow) # Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
existing_child_names = set() existing_child_names = get_own_child_object_names(obj_file)
m_co = re.search(r'(?s)<ChildObjects>(.*?)</ChildObjects>', obj_content)
if m_co:
for nm in re.findall(r'<Name>(\w+)</Name>', m_co.group(1)):
existing_child_names.add(nm)
insert_attrs = [a for a in src_attrs if a["Name"] not in existing_child_names] insert_attrs = [a for a in src_attrs if a["Name"] not in existing_child_names]
insert_ts = [t for t in src_ts if t["Name"] not in existing_child_names] insert_ts = [t for t in src_ts if t["Name"] not in existing_child_names]
# Generate full object XML with attributes and TS # Generate full object XML with attributes and TS
content_parts = [] content_parts = []
for attr in insert_attrs: for attr in insert_attrs:
content_parts.append(build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t")) content_parts.append(build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t", attr.get("Kind", "Attribute")))
for ts in insert_ts: for ts in insert_ts:
content_parts.append(build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t")) content_parts.append(build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t"))
adopted_content = "\n".join(content_parts).rstrip() adopted_content = "\n".join(content_parts).rstrip()
@@ -1161,19 +1452,9 @@ def main():
if props_xml: if props_xml:
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}", 1) obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}", 1)
# Replace empty ChildObjects with adopted content # Добавить заимствованное содержимое в ChildObjects объекта (там уже может лежать <Form>)
if adopted_content: if adopted_content:
# Handle <ChildObjects/> (self-closing) obj_content = insert_into_own_child_objects(obj_content, f"\r\n{adopted_content}")
if re.search(r'<ChildObjects\s*/>', obj_content):
obj_content = re.sub(r'<ChildObjects\s*/>', f"<ChildObjects>\r\n{adopted_content}\r\n\t\t</ChildObjects>", obj_content)
# Handle <ChildObjects>...</ChildObjects> (may already have Form entry)
elif re.search(r'(?s)<ChildObjects>(.*?)</ChildObjects>', obj_content):
m = re.search(r'(?s)<ChildObjects>(.*?)</ChildObjects>', obj_content)
existing_inner = m.group(1)
obj_content = obj_content.replace(
f"<ChildObjects>{existing_inner}</ChildObjects>",
f"<ChildObjects>{existing_inner}\r\n{adopted_content}\r\n\t\t</ChildObjects>"
)
write_utf8_bom(obj_file, obj_content) write_utf8_bom(obj_file, obj_content)
info(f" Enriched object: {obj_file}") info(f" Enriched object: {obj_file}")
@@ -1185,6 +1466,18 @@ def main():
for ts in src_ts: for ts in src_ts:
for tsa in ts["Attributes"]: for tsa in ts["Attributes"]:
all_type_xmls.append(tsa["TypeXml"]) all_type_xmls.append(tsa["TypeXml"])
# Типы из <Columns> основного реквизита формы: колонку мы переносим (borrow_form), значит и её
# тип должен быть заимствован — иначе колонка ссылается на DefinedType/справочник, которого в
# расширении нет. Конфигуратор поступает так же (эталон: DefinedTypes/Артикул при заимствовании
# формы заказа поставщику).
src_form_for_cols = os.path.join(cfg_dir, dir_name, obj_name, "Forms", form_name, "Ext", "Form.xml")
if os.path.isfile(src_form_for_cols):
cols_tree = etree.parse(src_form_for_cols)
cols_ns_strip = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
cols_info = get_main_attribute_info(cols_tree.getroot(), cols_ns_strip)
if cols_info:
all_type_xmls.extend(re.findall(r'(?s)<Columns>.*?</Columns>', cols_info["Xml"]))
ref_types = collect_reference_types(all_type_xmls) ref_types = collect_reference_types(all_type_xmls)
info(f" Reference types to borrow: {len(ref_types)}") info(f" Reference types to borrow: {len(ref_types)}")
@@ -1363,37 +1656,72 @@ def main():
# (e.g. a 2.13 form inside a 2.17 extension). The platform upgrades the form to the root version. # (e.g. a 2.13 form inside a 2.17 extension). The platform upgrades the form to the root version.
form_version = format_version form_version = format_version
# Секции формы отбираются по имени, а не по позиции: свойства лежат и до, и после
# <CommandSet> (корпусная проверка: у всех 794 форм документов ERP с CommandSet он стоит
# раньше AutoCommandBar, а AutoTime/UsePostingMode/RepostOnWrite — после него). Позиционная
# отсечка теряла весь хвост, и платформа молча подставляла дефолты вместо потерянных свойств.
src_auto_cmd = None src_auto_cmd = None
form_props = [] form_props = []
reached_visual = False
for fc in src_form_el: for fc in src_form_el:
if not isinstance(fc.tag, str): if not isinstance(fc.tag, str):
continue continue
ln = localname(fc) ln = localname(fc)
if ln == "AutoCommandBar" and src_auto_cmd is None: if ln == "AutoCommandBar" and src_auto_cmd is None:
reached_visual = True
src_auto_cmd = fc src_auto_cmd = fc
continue continue
if ln in ("ChildItems", "Events", "Attributes", "Commands", "Parameters", "CommandSet"): # ChildItems забирается отдельным поиском ниже
reached_visual = True if ln == "ChildItems":
continue continue
if not reached_visual: # Структурные секции: в расширении их содержимое недействительно (обработчики, команды и
# Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.) # параметры базовой формы, ссылки командного интерфейса на команды базовой конфигурации).
form_props.append(decode_numeric_entities(etree.tostring(fc, encoding="unicode"))) if ln in FORM_STRUCTURAL_SECTIONS:
continue
# Свойства, значение которых — имя реквизита формы. Реквизиты в заимствованную форму не
# переносятся, поэтому Конфигуратор такие свойства выбрасывает (проверено на форме отчёта:
# ReportResult и DetailsData выброшены, CustomSettingsFolder — имя элемента — сохранён).
if ln in FORM_ATTRIBUTE_REF_PROPS:
continue
# with_tail=False — хвостовой пробел принадлежит родителю; с ним в вывод попадали
# пустые строки, которых нет у PS-порта (OuterXml хвост не включает).
form_props.append(decode_numeric_entities(etree.tostring(fc, encoding="unicode", with_tail=False)))
ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"') ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
# uuid реквизитов объекта — только для формы без заимствованного основного реквизита:
# там ссылки параметров выбора переводятся на непрозрачную форму пути
# Имя основного реквизита источника нужно в обоих режимах: по нему опознаётся корень путей
# в ссылках параметров выбора. А main_attr_name управляет вырезанием привязок и потому
# остаётся пустым в скелетном режиме — там привязки снимаются все.
src_main_info = get_main_attribute_info(src_form_el, ns_strip_pattern)
src_main_attr_name = src_main_info["Name"] if src_main_info else ""
form_attr_ids = get_form_attribute_ids(src_form_el)
# Основной реквизит исходной формы: его имя — корень путей к данным, которые нужно сохранить
# («Объект.» у формы объекта, «Список.» у формы списка, «Запись.» у формы записи регистра)
main_attr_info = src_main_info if borrow_main_attr else None
# Имена реквизитов объекта нужны в обоих режимах: без заимствования — чтобы построить
# непрозрачный путь, с заимствованием — чтобы отличить реквизит (разрешается текстом) от
# стандартного поля (не разрешается)
src_attr_uuids = get_source_attribute_uuids(type_name, obj_name)
main_attr_name = main_attr_info["Name"] if main_attr_info else ""
if borrow_main_attr and main_attr_info is None:
warn(" У формы нет основного реквизита — -BorrowMainAttribute проигнорирован")
# AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false # AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false
auto_cmd_xml = "" auto_cmd_xml = ""
if src_auto_cmd is not None: if src_auto_cmd is not None:
auto_cmd_xml = decode_numeric_entities(etree.tostring(src_auto_cmd, encoding="unicode")) auto_cmd_xml = decode_numeric_entities(etree.tostring(src_auto_cmd, encoding="unicode", with_tail=False))
auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml) auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml)
auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml) auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml)
auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>') auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>')
# Strip ExcludedCommand (references to standard commands invalid in extension) # Вложенный CommandSet выбрасывается целиком, а не опустошается: Конфигуратор в заимствованной
auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml) # форме оставляет только корневой (тот идёт свойством формы, здесь его нет).
auto_cmd_xml = re.sub(r'(?s)\s*<CommandSet>.*?</CommandSet>', '', auto_cmd_xml)
auto_cmd_xml = re.sub(r'\s*<CommandSet/>', '', auto_cmd_xml)
# Strip data-binding tags whose root attribute isn't borrowed # Strip data-binding tags whose root attribute isn't borrowed
auto_cmd_xml = strip_form_bindings(auto_cmd_xml, borrow_main_attr) auto_cmd_xml = strip_form_bindings(auto_cmd_xml, main_attr_name)
auto_cmd_xml = rewrite_choice_parameter_links(
auto_cmd_xml, src_attr_uuids, form_attr_ids, src_main_attr_name, main_attr_info is not None)
# ChildItems: copy full tree, clean up base-config references # ChildItems: copy full tree, clean up base-config references
child_items_xml = "" child_items_xml = ""
@@ -1404,14 +1732,17 @@ def main():
break break
if src_child_items is not None: if src_child_items is not None:
child_items_xml = decode_numeric_entities(etree.tostring(src_child_items, encoding="unicode")) child_items_xml = decode_numeric_entities(etree.tostring(src_child_items, encoding="unicode", with_tail=False))
child_items_xml = ns_strip_pattern.sub("", child_items_xml) child_items_xml = ns_strip_pattern.sub("", child_items_xml)
# Replace all CommandName values with 0 # Replace all CommandName values with 0
child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml) child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml)
# Strip data-binding tags whose root attribute isn't borrowed # Strip data-binding tags whose root attribute isn't borrowed
child_items_xml = strip_form_bindings(child_items_xml, borrow_main_attr) child_items_xml = strip_form_bindings(child_items_xml, main_attr_name)
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension) child_items_xml = rewrite_choice_parameter_links(
child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', child_items_xml) child_items_xml, src_attr_uuids, form_attr_ids, src_main_attr_name, main_attr_info is not None)
# Вложенные CommandSet (у таблиц, полей табличного документа и т.п.) — целиком, см. выше
child_items_xml = re.sub(r'(?s)\s*<CommandSet>.*?</CommandSet>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<CommandSet/>', '', child_items_xml)
# Strip TypeLink blocks with human-readable DataPath (Items.XXX) # Strip TypeLink blocks with human-readable DataPath (Items.XXX)
child_items_xml = re.sub(r'\s*<TypeLink>\s*<xr:DataPath>Items\.[^<]*</xr:DataPath>.*?</TypeLink>', '', child_items_xml, flags=re.DOTALL) child_items_xml = re.sub(r'\s*<TypeLink>\s*<xr:DataPath>Items\.[^<]*</xr:DataPath>.*?</TypeLink>', '', child_items_xml, flags=re.DOTALL)
# Strip element-level Events # Strip element-level Events
@@ -1594,20 +1925,9 @@ def main():
parts.append(f"\t{child_items_xml}\r\n") parts.append(f"\t{child_items_xml}\r\n")
# Attributes: empty or with MainAttribute when borrow_main_attr # Attributes: empty or with MainAttribute when borrow_main_attr
if borrow_main_attr: if borrow_main_attr and main_attr_info:
obj_type_prefix = ""
gt_list = GENERATED_TYPES.get(type_name, [])
for g in gt_list:
if g["category"] == "Object":
obj_type_prefix = g["prefix"]
break
main_attr_type = f"cfg:{obj_type_prefix}.{obj_name}"
parts.append("\t<Attributes>\r\n") parts.append("\t<Attributes>\r\n")
parts.append('\t\t<Attribute name="\u041e\u0431\u044a\u0435\u043a\u0442" id="1000001">\r\n') parts.append(f"\t\t{main_attr_info['Xml']}\r\n")
parts.append(f"\t\t\t<Type><v8:Type>{main_attr_type}</v8:Type></Type>\r\n")
parts.append("\t\t\t<MainAttribute>true</MainAttribute>\r\n")
parts.append("\t\t\t<SavedData>true</SavedData>\r\n")
parts.append("\t\t</Attribute>\r\n")
parts.append("\t</Attributes>") parts.append("\t</Attributes>")
else: else:
parts.append("\t<Attributes/>") parts.append("\t<Attributes/>")
@@ -1637,13 +1957,12 @@ def main():
parts.append("\r\n") parts.append("\r\n")
# BaseForm Attributes: same as main section # BaseForm Attributes: same as main section
if borrow_main_attr: if borrow_main_attr and main_attr_info:
parts.append("\t\t<Attributes>\r\n") parts.append("\t\t<Attributes>\r\n")
parts.append('\t\t\t<Attribute name="\u041e\u0431\u044a\u0435\u043a\u0442" id="1000001">\r\n') # В BaseForm та же секция на уровень глубже — приём переиндентации тот же, что у ChildItems
parts.append(f"\t\t\t\t<Type><v8:Type>{main_attr_type}</v8:Type></Type>\r\n") for li, line in enumerate(main_attr_info['Xml'].split('\n')):
parts.append("\t\t\t\t<MainAttribute>true</MainAttribute>\r\n") parts.append(f"\t\t\t{line}" if li == 0 else f"\t{line}")
parts.append("\t\t\t\t<SavedData>true</SavedData>\r\n") parts.append("\r\n")
parts.append("\t\t\t</Attribute>\r\n")
parts.append("\t\t</Attributes>") parts.append("\t\t</Attributes>")
else: else:
parts.append("\t\t<Attributes/>") parts.append("\t\t<Attributes/>")
@@ -1656,6 +1975,10 @@ def main():
form_xml_file = os.path.join(form_xml_dir, "Form.xml") form_xml_file = os.path.join(form_xml_dir, "Form.xml")
write_xml_file(form_xml_file, "".join(parts)) write_xml_file(form_xml_file, "".join(parts))
info(f" Created: {form_xml_file}") info(f" Created: {form_xml_file}")
if DROPPED_LINKS:
uniq = sorted(set(DROPPED_LINKS))
warn(f" Вырезано связей параметров выбора: {len(uniq)} — путь не разрешается в расширении: {', '.join(uniq)}")
DROPPED_LINKS.clear()
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must # 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
# not clobber user code added to the form module). # not clobber user code added to the form module).
@@ -1773,6 +2096,47 @@ def main():
borrowed_files.append(target_file) borrowed_files.append(target_file)
borrowed_count += 1 borrowed_count += 1
# --- Владельцы заимствованных справочников ---
# Ссылка в <Owners> должна вести на объект, который в расширении есть: иначе платформа падает
# при загрузке (проверено — access violation, не сообщение об ошибке). Конфигуратор владельца
# заимствует (эталон Issue66Example7_1: вместе со справочником перенесён и его ПВХ-владелец).
# Проход общий и повторяется, пока находятся новые: у владельца может быть свой владелец.
for _owner_pass in range(10):
new_owners = []
for root_dir, _dirs, files in os.walk(ext_dir):
for fn in files:
if not fn.endswith(".xml"):
continue
with open(os.path.join(root_dir, fn), "r", encoding="utf-8-sig") as fh:
shell_text = fh.read()
if "<Owners>" not in shell_text:
continue
for om in re.finditer(r'<xr:Item[^>]*>(\w+)\.(\w+)</xr:Item>', shell_text):
o_type, o_name = om.group(1), om.group(2)
if o_type not in CHILD_TYPE_DIR_MAP:
continue
if test_object_borrowed(o_type, o_name):
continue
if (o_type, o_name) in new_owners:
continue
new_owners.append((o_type, o_name))
if not new_owners:
break
for o_type, o_name in new_owners:
ow_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[o_type], f"{o_name}.xml")
if not os.path.isfile(ow_src_file):
warn(f" Владелец {o_type}.{o_name} не найден в источнике — ссылка останется висячей")
continue
ow_src = read_source_object(o_type, o_name)
ow_xml = build_borrowed_object_xml(o_type, o_name, ow_src["Uuid"], ow_src["Properties"])
ow_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[o_type])
os.makedirs(ow_dir, exist_ok=True)
ow_file = os.path.join(ow_dir, f"{o_name}.xml")
write_utf8_bom(ow_file, ow_xml)
add_to_child_objects(o_type, o_name)
borrowed_files.append(ow_file)
info(f" Auto-borrowed owner: {o_type}.{o_name}")
# --- Save modified Configuration.xml --- # --- Save modified Configuration.xml ---
save_xml_bom(tree, ext_resolved) save_xml_bom(tree, ext_resolved)
info(f"Saved: {ext_resolved}") info(f"Saved: {ext_resolved}")
@@ -23,7 +23,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A python ".codex/skills/cfe-diff/scripts/cfe-diff.py" -ExtensionPath src\cfe\extname -ConfigPath src\cf -Mode A
``` ```
## Mode A — обзор расширения ## Mode A — обзор расширения
@@ -50,8 +50,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -Exte
```powershell ```powershell
# Обзор — что изменено в расширении # Обзор — что изменено в расширении
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Mode A
# Проверка переноса — все ли #Вставка перенесены # Проверка переноса — все ли #Вставка перенесены
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode B ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Mode B
``` ```
@@ -33,39 +33,39 @@ allowed-tools:
| `Name` | Имя расширения (обязат.) | — | | `Name` | Имя расширения (обязат.) | — |
| `Synonym` | Синоним | = Name | | `Synonym` | Синоним | = Name |
| `NamePrefix` | Префикс собственных объектов | = Name + "_" | | `NamePrefix` | Префикс собственных объектов | = Name + "_" |
| `OutputDir` | Каталог для создания | `src` | | `OutputDir` | Каталог для создания; клади расширение в свой подкаталог, названный по имени: `src\cfe\<Name>` | `src` |
| `Purpose` | `Patch` (исправление) / `Customization` (доработка) / `AddOn` (дополнение) | `Customization` | | `Purpose` | `Patch` (исправление) / `Customization` (доработка) / `AddOn` (дополнение) | `Customization` |
| `Version` | Версия расширения | — | | `Version` | Версия расширения | — |
| `Vendor` | Поставщик | — | | `Vendor` | Поставщик | — |
| `CompatibilityMode` | Режим совместимости | `Version8_3_24` | | `CompatibilityMode` | Режим совместимости; при заданном `ConfigPath` определяется по базовой конфигурации и этот параметр не нужен | `Version8_3_24` |
| `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — | | `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — |
| `NoRole` | Без основной роли | false | | `NoRole` | Без основной роли | false |
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-init.ps1" -Name "МоёРасширение" python ".codex/skills/cfe-init/scripts/cfe-init.py" -Name "МоёРасширение" -OutputDir "src\cfe\МоёРасширение" -ConfigPath "src\cf"
``` ```
## Примеры ## Примеры
```powershell ```powershell
# Расширение для ERP с авто-определением совместимости из базовой конфигурации # Расширение для ERP с авто-определением совместимости из базовой конфигурации
... -Name Расш1 -ConfigPath C:\WS\tasks\cfsrc\erp_8.3.24 -OutputDir src ... -Name Расш1 -ConfigPath src\cf -OutputDir src\cfe\Расш1
# Расширение-исправление с явным режимом совместимости # Расширение-исправление с явным режимом совместимости
... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src ... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src\cfe\Расш1
# Расширение-доработка с версией # Расширение-доработка с версией
... -Name МоёРасширение -Version "1.0.0.1" -Vendor "Компания" -OutputDir src ... -Name МоёРасширение -Version "1.0.0.1" -Vendor "Компания" -OutputDir src\cfe\МоёРасширение
# Без роли, с явным префиксом # Без роли, с явным префиксом
... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -NoRole -OutputDir src ... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -NoRole -OutputDir src\cfe\ИсправлениеБага
``` ```
## Верификация ## Верификация
``` ```
/cfe-validate <OutputDir> /cfe-validate <OutputDir> -ConfigPath <ConfigPath>
``` ```
@@ -110,36 +110,36 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before python ".codex/skills/cfe-patch-method/scripts/cfe-patch-method.py" -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
``` ```
## Примеры ## Примеры
```powershell ```powershell
# Код перед записью # Код перед записью
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
# Перехват После на форме # Перехват После на форме
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After
# Замена функции (ПродолжитьВызов) # Замена функции (ПродолжитьВызов)
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType Instead ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType Instead
# ИзменениеИКонтроль — копия тела для правки маркерами # ИзменениеИКонтроль — копия тела для правки маркерами
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Document.РеализацияТоваров.ObjectModule" -MethodName "ОбработкаПроведения" -InterceptorType ModificationAndControl ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -ModulePath "Document.РеализацияТоваров.ObjectModule" -MethodName "ОбработкаПроведения" -InterceptorType ModificationAndControl
# ModulePath как путь к файлу модуля-источника (без -ConfigPath) # ModulePath как путь к файлу модуля-источника (без -ConfigPath)
... -ExtensionPath src\cfe\ИмяРасширения -ModulePath "src\cf\CommonModules\ОбщийМодуль\Ext\Module.bsl" -MethodName "ПолучитьДанные" -InterceptorType Instead ... -ExtensionPath src\cfe\extname -ModulePath "src\cf\CommonModules\ОбщийМодуль\Ext\Module.bsl" -MethodName "ПолучитьДанные" -InterceptorType Instead
# Проверить все контролируемые методы расширения на дрейф # Проверить все контролируемые методы расширения на дрейф
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -Check ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Check
# Актуализировать дрейфнувшие контролируемые методы пачкой # Актуализировать дрейфнувшие контролируемые методы пачкой
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -Actualize ... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Actualize
``` ```
## Верификация ## Верификация
``` ```
/cfe-validate <ExtensionPath> /cfe-validate <ExtensionPath> -ConfigPath <ConfigPath>
``` ```
@@ -1,7 +1,7 @@
--- ---
name: cfe-validate name: cfe-validate
description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности
argument-hint: <ExtensionPath> [-Detailed] [-MaxErrors 30] argument-hint: <ExtensionPath> [-ConfigPath <ConfigDir>] [-Detailed] [-MaxErrors 30]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
@@ -17,13 +17,24 @@ allowed-tools:
| Параметр | Обяз. | Умолч. | Описание | | Параметр | Обяз. | Умолч. | Описание |
|---------------|:-----:|---------|-------------------------------------------------| |---------------|:-----:|---------|-------------------------------------------------|
| ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения | | ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения |
| ConfigPath | нет | — | Каталог конфигурации, из которой заимствованы объекты |
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | | Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
| MaxErrors | нет | 30 | Остановиться после N ошибок | | MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл | | OutFile | нет | — | Записать результат в файл |
### ConfigPath
Указывай всегда, когда конфигурация-источник доступна: без неё часть ошибок заимствованных форм не ловится, и расширение может пройти валидацию, а потом быть отвергнутым платформой при загрузке.
Если пользователь не указал путь — определи сам:
1. Прочитай `.v8-project.json` из корня проекта
2. Разреши целевую базу (по имени, ветке или `default`)
3. Возьми её поле `configSrc`
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src" python ".codex/skills/cfe-validate/scripts/cfe-validate.py" -ExtensionPath "src\cfe\extname"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src/Configuration.xml" python ".codex/skills/cfe-validate/scripts/cfe-validate.py" -ExtensionPath "src\cfe\extname\Configuration.xml"
python ".codex/skills/cfe-validate/scripts/cfe-validate.py" -ExtensionPath "src\cfe\extname" -ConfigPath "src\cf"
``` ```
@@ -1,4 +1,4 @@
# cfe-validate v1.7 — Validate 1C configuration extension structure (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы) # cfe-validate v1.10 — Validate 1C configuration extension structure (CFE) (полнота GeneratedType, ТЧ из AdditionalColumns, сверка путей с -ConfigPath)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -9,7 +9,11 @@ param(
[int]$MaxErrors = 30, [int]$MaxErrors = 30,
[string]$OutFile [string]$OutFile,
# Конфигурация-источник. Без неё проверки, требующие сравнения с основной конфигурацией,
# пропускаются (о чём сказано в отчёте), остальные работают как раньше.
[string]$ConfigPath
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
@@ -89,6 +93,19 @@ $finalize = {
} }
} }
# --- Format version ---
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
$formatVerifiedMin = "2.17"
$formatVerifiedMax = "2.21"
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
# --- Reference tables --- # --- Reference tables ---
$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' $guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$' $identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
@@ -144,6 +161,46 @@ $childTypeDirMap = @{
"IntegrationService"="IntegrationServices" "IntegrationService"="IntegrationServices"
} }
# Наборы GeneratedType по типу объекта (эталон — таблица §2.5 спецификации конфигурации).
# Неполный набор в заимствованной оболочке платформа отвергает при загрузке: «отсутствует один
# или более типов объекта <Тип>». Типы, у которых GeneratedType нет вовсе (общие модули,
# подписки, регламентные задания и т.п.), в карте отсутствуют — для них проверка не выполняется.
$generatedTypeCategories = @{
"Catalog" = @("Object","Ref","Selection","List","Manager")
"Document" = @("Object","Ref","Selection","List","Manager")
"Enum" = @("Ref","Manager","List")
"Constant" = @("Manager","ValueManager","ValueKey")
"Report" = @("Object","Manager")
"DataProcessor" = @("Object","Manager")
"ExchangePlan" = @("Object","Ref","Selection","List","Manager")
"Task" = @("Object","Ref","Selection","List","Manager")
"BusinessProcess" = @("Object","Ref","Selection","List","Manager","RoutePointRef")
"ChartOfCharacteristicTypes" = @("Object","Ref","Selection","List","Manager","Characteristic")
"ChartOfAccounts" = @("Object","Ref","Selection","List","Manager","ExtDimensionTypes","ExtDimensionTypesRow")
"ChartOfCalculationTypes" = @("Object","Ref","Selection","List","Manager","DisplacingCalculationTypes","DisplacingCalculationTypesRow","BaseCalculationTypes","BaseCalculationTypesRow","LeadingCalculationTypes","LeadingCalculationTypesRow")
"InformationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","RecordManager")
"AccumulationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey")
"AccountingRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","ExtDimensions")
"CalculationRegister" = @("Record","Manager","Selection","List","RecordSet","RecordKey","Recalcs")
"DocumentJournal" = @("Selection","List","Manager")
"Sequence" = @("Record","Manager","RecordSet")
"FilterCriterion" = @("Manager","List")
"SettingsStorage" = @("Manager")
"IntegrationService" = @("Manager")
"WSReference" = @("Manager")
"DefinedType" = @("DefinedType")
}
# Стандартные реквизиты объектов: в ChildObjects их нет, но пути Объект.<Стандартный> законны.
# Имена зависят от варианта встроенного языка, поэтому держим оба написания.
$script:standardObjectFields = @(
"Code","Description","Ref","Parent","Owner","DeletionMark","Predefined","IsFolder","LineNumber",
"Number","Date","Posted","PredefinedDataName","RegisterRecords","DataVersion","RowsCount",
"Код","Наименование","Ссылка","Родитель","Владелец","ПометкаУдаления","Предопределенный",
"ЭтоГруппа","НомерСтроки","Номер","Дата","Проведен","ИмяПредопределенныхДанных",
"Движения","ВерсияДанных","КоличествоСтрок"
)
# Valid enum values for extension properties # Valid enum values for extension properties
$validEnumValues = @{ $validEnumValues = @{
"ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1") "ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1")
@@ -195,11 +252,15 @@ if ($root.NamespaceURI -ne $expectedNs) {
} }
$version = $root.GetAttribute("version") $version = $root.GetAttribute("version")
$versionRank = Get-FormatRank $version
if (-not $version) { if (-not $version) {
Report-Warn "1. Missing version attribute on MetaDataObject" Report-Warn "1. Missing version attribute on MetaDataObject"
} elseif ($version -notin @("2.17", "2.18", "2.19", "2.20", "2.21")) { } elseif ($versionRank -eq 0) {
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27). Report-Error "1. Malformed version '$version' (expected N.N)"
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20 or 2.21)" } elseif ($versionRank -lt (Get-FormatRank $formatVerifiedMin)) {
Report-Warn "1. Format version '$version' is below the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
} elseif ($versionRank -gt (Get-FormatRank $formatVerifiedMax)) {
Report-Warn "1. Format version '$version' is above the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
} }
# Must have Configuration child # Must have Configuration child
@@ -537,6 +598,7 @@ if ($script:stopped) { & $finalize; exit 1 }
# --- Check 9: Borrowed objects validation + Check 10: Sub-items --- # --- Check 9: Borrowed objects validation + Check 10: Sub-items ---
$script:enumValuesIndex = @{} $script:enumValuesIndex = @{}
$script:borrowedTSIndex = @{}
$script:formList = @() $script:formList = @()
# Helper: check if sub-item has explicit borrowed metadata # Helper: check if sub-item has explicit borrowed metadata
@@ -640,6 +702,25 @@ if ($childObjNode) {
} else { } else {
$borrowedOk++ $borrowedOk++
} }
# Полнота набора GeneratedType: платформа отвергает оболочку с неполным набором
# («отсутствует один или более типов объекта ChartOfCharacteristicTypes»)
$expectedCats = $generatedTypeCategories[$typeName]
if ($expectedCats) {
$objInfo = $objEl.SelectSingleNode("md:InternalInfo", $objNs)
$foundCats = @{}
if ($objInfo) {
foreach ($gt in $objInfo.SelectNodes("xr:GeneratedType", $objNs)) {
$cat = $gt.GetAttribute("category")
if ($cat) { $foundCats[$cat] = $true }
}
}
$missingCats = @($expectedCats | Where-Object { -not $foundCats.ContainsKey($_) })
if ($missingCats.Count -gt 0) {
Report-Error "9. Borrowed ${typeName}.${childName}: missing GeneratedType categor$(if ($missingCats.Count -eq 1) { 'y' } else { 'ies' }) $($missingCats -join ', ')"
$check9Ok = $false
}
}
} }
# --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) --- # --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) ---
@@ -667,6 +748,12 @@ if ($childObjNode) {
$tsInfo = $subItem.SelectSingleNode("md:InternalInfo", $objNs) $tsInfo = $subItem.SelectSingleNode("md:InternalInfo", $objNs)
$tsName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs) $tsName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs)
$tsLabel = if ($tsName) { $tsName.InnerText } else { "?" } $tsLabel = if ($tsName) { $tsName.InnerText } else { "?" }
# Индекс заимствованных ТЧ — по нему Check 12 сверяет <AdditionalColumns table="Объект.X">
if ($tsName) {
$tsKey = "${typeName}.${childName}"
if (-not $script:borrowedTSIndex.ContainsKey($tsKey)) { $script:borrowedTSIndex[$tsKey] = @{} }
$script:borrowedTSIndex[$tsKey][$tsName.InnerText] = $true
}
if (-not $tsInfo) { if (-not $tsInfo) {
Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing InternalInfo" Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing InternalInfo"
$check10Ok = $false $check10Ok = $false
@@ -896,6 +983,38 @@ foreach ($bf in $script:borrowedFormsWithTree) {
} }
} }
# Корень путей формы — имя её основного реквизита: «Объект» только у формы объекта, у формы
# списка «Список», у формы записи регистра «Запись». С зашитым «Объект» обе проверки ниже на
# таких формах молча не срабатывали. Ищем сначала в <Attributes> самой формы, потом в <BaseForm>.
$rootName = ""
$rootMatch = [regex]::Match($raw, '(?s)<Attribute name="([^"]+)"[^>]*>(?:(?!</Attribute>).)*?<MainAttribute>true</MainAttribute>')
if ($rootMatch.Success) { $rootName = $rootMatch.Groups[1].Value }
# <AdditionalColumns table="Объект.X"> — доп. колонки табличной части, объявленные в самой форме.
# Колонки есть, а самой ТЧ в расширении нет → платформа отвергает загрузку: «Неверный путь к
# данным» плюс «Колонки не могут быть добавлены к реквизиту».
$acTables = @{}
if ($rootName) {
$rootPat = [regex]::Escape($rootName)
foreach ($m in [regex]::Matches($raw, "<AdditionalColumns table=`"${rootPat}\.(\w+)`"")) {
$acTables[$m.Groups[1].Value] = $true
}
}
# Соседние проверки этого блока эвристичны (имя стиля добывается регуляркой), поэтому там
# предупреждение. Здесь сигнал точный — имя ТЧ берётся из атрибута, — а последствие жёсткое,
# поэтому ошибка.
if ($acTables.Count -gt 0) {
$ownerKey = ($ctx -split '\.Form\.')[0]
$ownerTS = $script:borrowedTSIndex[$ownerKey]
foreach ($tblName in $acTables.Keys) {
$depCheckCount++
if (-not $ownerTS -or -not $ownerTS.ContainsKey($tblName)) {
Report-Error "12. ${ctx}: <AdditionalColumns table=`"${rootName}.${tblName}`"> — TabularSection.${tblName} not borrowed in extension"
$check12Ok = $false
}
}
}
foreach ($mi in $missingItems) { foreach ($mi in $missingItems) {
Report-Warn "12. ${ctx}: references ${mi} not borrowed in extension" Report-Warn "12. ${ctx}: references ${mi} not borrowed in extension"
$check12Ok = $false $check12Ok = $false
@@ -931,6 +1050,127 @@ if ($script:borrowedFormsWithTree.Count -eq 0) {
Report-OK "13. TypeLink: clean" Report-OK "13. TypeLink: clean"
} }
# --- Check 14: пути Объект.* заимствованных форм против конфигурации-источника ---
# Требует -ConfigPath: отличить живой путь от висячего можно только по исходному объекту.
# «Объект.Партнер» валиден и без заимствования реквизита (наследуется от базы), а «Объект.Товары.Артикул»
# не разрешится нигде, если Артикул — не реквизит объекта и не колонка из <Columns> самой формы.
# Такой путь платформа отвергает на загрузке: «Неверный путь к данным».
if (-not $script:stopped -and $script:borrowedFormsWithTree.Count -gt 0) {
if (-not $ConfigPath) {
Out-Line "[INFO] 14. Пути Объект.* против конфигурации-источника не проверялись: не задан -ConfigPath"
} else {
$cfgRoot = $ConfigPath
if (-not [System.IO.Path]::IsPathRooted($cfgRoot)) { $cfgRoot = Join-Path (Get-Location).Path $cfgRoot }
if ((Test-Path $cfgRoot) -and -not (Test-Path $cfgRoot -PathType Container)) { $cfgRoot = Split-Path $cfgRoot -Parent }
if (-not (Test-Path (Join-Path $cfgRoot "Configuration.xml"))) {
Report-Warn "14. -ConfigPath '$ConfigPath': Configuration.xml не найден — проверка путей пропущена"
} else {
$check14Ok = $true
$pathCheckCount = 0
foreach ($bf in $script:borrowedFormsWithTree) {
$raw = $bf.RawText
$ctx = $bf.Context
# Корень путей — имя основного реквизита формы (см. проверку 12). Нет его ни в
# <Attributes> формы, ни в <BaseForm> — путей с корнем не бывает, проверять нечего.
$rootMatch14 = [regex]::Match($raw, '(?s)<Attribute name="([^"]+)"[^>]*>(?:(?!</Attribute>).)*?<MainAttribute>true</MainAttribute>')
if (-not $rootMatch14.Success) { continue }
$rootName = $rootMatch14.Groups[1].Value
# У динамического списка набор полей — результат его запроса, а не состав объекта:
# туда входят и стандартные поля списка (Ref, Date, DefaultPicture), и псевдонимы
# запроса. Сверять такие пути с ChildObjects объекта нельзя — будут ложные ошибки
# (корпусная проверка: 3383 таких сегмента на 1094 формах списка УТ).
if ($rootMatch14.Value -match '>cfg:DynamicList<') { continue }
$ownerKey = ($ctx -split '\.Form\.')[0]
$ownerParts = $ownerKey -split '\.', 2
if ($ownerParts.Count -lt 2) { continue }
$ownerType = $ownerParts[0]; $ownerName = $ownerParts[1]
$ownerDir = $childTypeDirMap[$ownerType]
if (-not $ownerDir) { continue }
$srcObjFile = Join-Path (Join-Path $cfgRoot $ownerDir) "${ownerName}.xml"
if (-not (Test-Path $srcObjFile)) {
Report-Warn "14. ${ctx}: объект-источник не найден в конфигурации ($ownerDir/${ownerName}.xml)"
continue
}
# Имена, доступные первым сегментом пути: реквизиты и ТЧ объекта-источника.
# Плюс для каждой ТЧ — её колонки: второй сегмент проверяем по ним (именно там
# и жил дефект — Объект.Товары.Артикул при живой ТЧ Товары).
$srcNames = @{}
$srcTSColumns = @{}
$srcDoc = New-Object System.Xml.XmlDocument
$srcDoc.PreserveWhitespace = $false
$srcDoc.Load($srcObjFile)
$srcObjEl = $null
foreach ($c in $srcDoc.DocumentElement.ChildNodes) {
if ($c.NodeType -eq 'Element') { $srcObjEl = $c; break }
}
$srcChildObjects = if ($srcObjEl) { $srcObjEl.SelectSingleNode("*[local-name()='ChildObjects']") } else { $null }
if ($srcChildObjects) {
foreach ($sub in $srcChildObjects.ChildNodes) {
if ($sub.NodeType -ne 'Element') { continue }
# У регистра дочерние объекты — Dimension/Resource, а не Attribute: без них замена
# корня превратила бы тихий пропуск в ложные ошибки на форме записи.
if ($sub.LocalName -notin @('Attribute','Dimension','Resource','TabularSection')) { continue }
$nameNode = $sub.SelectSingleNode("*[local-name()='Properties']/*[local-name()='Name']")
if (-not $nameNode) { continue }
$subName = $nameNode.InnerText.Trim()
$srcNames[$subName] = $true
if ($sub.LocalName -ne 'TabularSection') { continue }
$cols = @{}
foreach ($colName in $sub.SelectNodes("*[local-name()='ChildObjects']/*[local-name()='Attribute']/*[local-name()='Properties']/*[local-name()='Name']")) {
$cols[$colName.InnerText.Trim()] = $true
}
$srcTSColumns[$subName] = $cols
}
}
# Плюс колонки, объявленные в самой форме через <Columns>/<AdditionalColumns table="Объект.X">
$rootPat14 = [regex]::Escape($rootName)
foreach ($acm in [regex]::Matches($raw, "(?s)<AdditionalColumns table=`"${rootPat14}\.(\w+)`">(.*?)</AdditionalColumns>")) {
$tbl = $acm.Groups[1].Value
if (-not $srcTSColumns.ContainsKey($tbl)) { $srcTSColumns[$tbl] = @{} }
foreach ($cm in [regex]::Matches($acm.Groups[2].Value, '<Column name="(\w+)"')) {
$srcTSColumns[$tbl][$cm.Groups[1].Value] = $true
}
}
$badPaths = @{}
foreach ($m in [regex]::Matches($raw, "<(?:\w+:)?\w*DataPath[^>]*>${rootPat14}\.([^<]+)</(?:\w+:)?\w*DataPath>")) {
$segments = $m.Groups[1].Value -split '\.'
$seg0 = $segments[0]
$pathCheckCount++
if ($script:standardObjectFields -contains $seg0) { continue }
if (-not $srcNames.ContainsKey($seg0)) {
$badPaths["${rootName}.${seg0}"] = "у ${ownerKey} нет такого реквизита или табличной части"
continue
}
# Второй сегмент проверяем только для табличных частей: у ссылочного реквизита
# он ведёт в чужой объект, и это уже другая проверка.
if ($segments.Count -lt 2 -or -not $srcTSColumns.ContainsKey($seg0)) { continue }
$seg1 = $segments[1]
if ($script:standardObjectFields -contains $seg1) { continue }
# Итог колонки — псевдополе платформы: Total<Колонка> при живой колонке законен
if ($seg1 -like "Total*" -and $srcTSColumns[$seg0].ContainsKey($seg1.Substring(5))) { continue }
if (-not $srcTSColumns[$seg0].ContainsKey($seg1)) {
$badPaths["${rootName}.${seg0}.${seg1}"] = "у табличной части ${seg0} нет колонки ${seg1}, и <Columns> формы её не объявляет"
}
}
foreach ($bad in ($badPaths.Keys | Sort-Object)) {
Report-Error "14. ${ctx}: путь '${bad}' — $($badPaths[$bad])"
$check14Ok = $false
}
}
if ($check14Ok) {
Report-OK "14. Object paths vs source config: $pathCheckCount checked"
}
}
}
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here --- # --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
$extRootDir = Split-Path $resolvedPath -Parent $extRootDir = Split-Path $resolvedPath -Parent
$ctrlCount = 0 $ctrlCount = 0
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cfe-validate v1.7 — Validate 1C configuration extension XML structure (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы) # cfe-validate v1.10 — Validate 1C configuration extension XML structure (CFE) (полнота GeneratedType, ТЧ из AdditionalColumns, сверка путей с -ConfigPath)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects.""" """Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
import sys, os, argparse, re import sys, os, argparse, re
@@ -96,6 +96,50 @@ CHILD_TYPE_DIR_MAP = {
'IntegrationService': 'IntegrationServices', 'IntegrationService': 'IntegrationServices',
} }
# Наборы GeneratedType по типу объекта (эталон — таблица §2.5 спецификации конфигурации).
# Неполный набор в заимствованной оболочке платформа отвергает при загрузке: «отсутствует один
# или более типов объекта <Тип>». Типы, у которых GeneratedType нет вовсе (общие модули,
# подписки, регламентные задания и т.п.), в карте отсутствуют — для них проверка не выполняется.
GENERATED_TYPE_CATEGORIES = {
'Catalog': ['Object', 'Ref', 'Selection', 'List', 'Manager'],
'Document': ['Object', 'Ref', 'Selection', 'List', 'Manager'],
'Enum': ['Ref', 'Manager', 'List'],
'Constant': ['Manager', 'ValueManager', 'ValueKey'],
'Report': ['Object', 'Manager'],
'DataProcessor': ['Object', 'Manager'],
'ExchangePlan': ['Object', 'Ref', 'Selection', 'List', 'Manager'],
'Task': ['Object', 'Ref', 'Selection', 'List', 'Manager'],
'BusinessProcess': ['Object', 'Ref', 'Selection', 'List', 'Manager', 'RoutePointRef'],
'ChartOfCharacteristicTypes': ['Object', 'Ref', 'Selection', 'List', 'Manager', 'Characteristic'],
'ChartOfAccounts': ['Object', 'Ref', 'Selection', 'List', 'Manager', 'ExtDimensionTypes', 'ExtDimensionTypesRow'],
'ChartOfCalculationTypes': ['Object', 'Ref', 'Selection', 'List', 'Manager', 'DisplacingCalculationTypes', 'DisplacingCalculationTypesRow', 'BaseCalculationTypes', 'BaseCalculationTypesRow', 'LeadingCalculationTypes', 'LeadingCalculationTypesRow'],
'InformationRegister': ['Record', 'Manager', 'Selection', 'List', 'RecordSet', 'RecordKey', 'RecordManager'],
'AccumulationRegister': ['Record', 'Manager', 'Selection', 'List', 'RecordSet', 'RecordKey'],
'AccountingRegister': ['Record', 'Manager', 'Selection', 'List', 'RecordSet', 'RecordKey', 'ExtDimensions'],
'CalculationRegister': ['Record', 'Manager', 'Selection', 'List', 'RecordSet', 'RecordKey', 'Recalcs'],
'DocumentJournal': ['Selection', 'List', 'Manager'],
'Sequence': ['Record', 'Manager', 'RecordSet'],
'FilterCriterion': ['Manager', 'List'],
'SettingsStorage': ['Manager'],
'IntegrationService': ['Manager'],
'WSReference': ['Manager'],
'DefinedType': ['DefinedType'],
}
# Стандартные реквизиты объектов: в ChildObjects их нет, но пути Объект.<Стандартный> законны.
# Имена зависят от варианта встроенного языка, поэтому держим оба написания.
# Основной реквизит формы: <Attribute name="X"> с <MainAttribute>true</MainAttribute> внутри
MAIN_ATTR_RE = re.compile(
r'<Attribute name=\"([^\"]+)\"[^>]*>(?:(?!</Attribute>).)*?<MainAttribute>true</MainAttribute>', re.DOTALL)
STANDARD_OBJECT_FIELDS = {
'Code', 'Description', 'Ref', 'Parent', 'Owner', 'DeletionMark', 'Predefined', 'IsFolder', 'LineNumber',
'Number', 'Date', 'Posted', 'PredefinedDataName', 'RegisterRecords', 'DataVersion', 'RowsCount',
'Код', 'Наименование', 'Ссылка', 'Родитель', 'Владелец', 'ПометкаУдаления', 'Предопределенный',
'ЭтоГруппа', 'НомерСтроки', 'Номер', 'Дата', 'Проведен', 'ИмяПредопределенныхДанных',
'Движения', 'ВерсияДанных', 'КоличествоСтрок',
}
# Valid enum values for extension properties # Valid enum values for extension properties
VALID_ENUM_VALUES = { VALID_ENUM_VALUES = {
'ConfigurationExtensionCompatibilityMode': [ 'ConfigurationExtensionCompatibilityMode': [
@@ -117,6 +161,20 @@ VALID_ENUM_VALUES = {
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses' EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
# ── Format version ───────────────────────────────────────────
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
FORMAT_VERIFIED_MIN = "2.17"
FORMAT_VERIFIED_MAX = "2.21"
def format_rank(ver):
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
class Reporter: class Reporter:
def __init__(self, max_errors, detailed=False): def __init__(self, max_errors, detailed=False):
@@ -177,11 +235,15 @@ def main():
parser.add_argument('-Detailed', action='store_true') parser.add_argument('-Detailed', action='store_true')
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30) parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
parser.add_argument('-OutFile', dest='OutFile', default='') parser.add_argument('-OutFile', dest='OutFile', default='')
# Конфигурация-источник. Без неё проверки, требующие сравнения с основной конфигурацией,
# пропускаются (о чём сказано в отчёте), остальные работают как раньше.
parser.add_argument('-ConfigPath', dest='ConfigPath', default='')
args = ci_parse_args(parser) args = ci_parse_args(parser)
extension_path = args.ExtensionPath extension_path = args.ExtensionPath
max_errors = args.MaxErrors max_errors = args.MaxErrors
out_file = args.OutFile out_file = args.OutFile
config_path_arg = args.ConfigPath
# --- Resolve path --- # --- Resolve path ---
if not os.path.isabs(extension_path): if not os.path.isabs(extension_path):
@@ -237,11 +299,17 @@ def main():
check1_ok = False check1_ok = False
version = root.get('version', '') version = root.get('version', '')
version_rank = format_rank(version)
if not version: if not version:
r.warn('1. Missing version attribute on MetaDataObject') r.warn('1. Missing version attribute on MetaDataObject')
elif version not in ('2.17', '2.18', '2.19', '2.20', '2.21'): elif version_rank == 0:
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27). r.error(f"1. Malformed version '{version}' (expected N.N)")
r.warn(f"1. Unusual version '{version}' (expected 2.17-2.20 or 2.21)") elif version_rank < format_rank(FORMAT_VERIFIED_MIN):
r.warn(f"1. Format version '{version}' is below the tested range "
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
elif version_rank > format_rank(FORMAT_VERIFIED_MAX):
r.warn(f"1. Format version '{version}' is above the tested range "
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
# Must have Configuration child # Must have Configuration child
cfg_node = None cfg_node = None
@@ -560,6 +628,7 @@ def main():
MD = NS['md'] MD = NS['md']
XR = NS['xr'] XR = NS['xr']
enum_values_index = {} enum_values_index = {}
borrowed_ts_index = {}
form_list = [] form_list = []
def is_borrowed_sub_item(sub_item): def is_borrowed_sub_item(sub_item):
@@ -659,6 +728,23 @@ def main():
else: else:
borrowed_ok_count += 1 borrowed_ok_count += 1
# Полнота набора GeneratedType: платформа отвергает оболочку с неполным набором
# («отсутствует один или более типов объекта ChartOfCharacteristicTypes»)
expected_cats = GENERATED_TYPE_CATEGORIES.get(type_name)
if expected_cats:
obj_info = obj_el.find(f'{{{MD}}}InternalInfo')
found_cats = set()
if obj_info is not None:
for gt in obj_info.findall(f'{{{XR}}}GeneratedType'):
cat = gt.get('category')
if cat:
found_cats.add(cat)
missing_cats = [c for c in expected_cats if c not in found_cats]
if missing_cats:
word = 'category' if len(missing_cats) == 1 else 'categories'
r.error(f"9. Borrowed {type_name}.{child_name}: missing GeneratedType {word} {', '.join(missing_cats)}")
check9_ok = False
# --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) --- # --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) ---
obj_child_objects = obj_el.find(f'{{{MD}}}ChildObjects') obj_child_objects = obj_el.find(f'{{{MD}}}ChildObjects')
if obj_child_objects is not None: if obj_child_objects is not None:
@@ -686,6 +772,9 @@ def main():
ts_info = sub_item.find(f'{{{MD}}}InternalInfo') ts_info = sub_item.find(f'{{{MD}}}InternalInfo')
ts_name_el = sub_item.find(f'{{{MD}}}Properties/{{{MD}}}Name') ts_name_el = sub_item.find(f'{{{MD}}}Properties/{{{MD}}}Name')
ts_label = (ts_name_el.text or '?') if ts_name_el is not None else '?' ts_label = (ts_name_el.text or '?') if ts_name_el is not None else '?'
# Индекс заимствованных ТЧ — по нему Check 12 сверяет <AdditionalColumns table="Объект.X">
if ts_name_el is not None and ts_name_el.text:
borrowed_ts_index.setdefault(f'{type_name}.{child_name}', {})[ts_name_el.text.strip()] = True
if ts_info is None: if ts_info is None:
r.error(f'10. {ctx}: TabularSection.{ts_label} missing InternalInfo') r.error(f'10. {ctx}: TabularSection.{ts_label} missing InternalInfo')
check10_ok = False check10_ok = False
@@ -878,6 +967,29 @@ def main():
elif entry['Enum'] not in enum_values_index or entry['Value'] not in enum_values_index.get(entry['Enum'], {}): elif entry['Enum'] not in enum_values_index or entry['Value'] not in enum_values_index.get(entry['Enum'], {}):
missing_items.append(f"Enum.{entry['Enum']}.EnumValue.{entry['Value']}") missing_items.append(f"Enum.{entry['Enum']}.EnumValue.{entry['Value']}")
# <AdditionalColumns table="Объект.X"> — доп. колонки табличной части, объявленные в самой форме.
# Колонки есть, а самой ТЧ в расширении нет → платформа отвергает загрузку: «Неверный путь к
# данным» плюс «Колонки не могут быть добавлены к реквизиту».
# Соседние проверки этого блока эвристичны (имя стиля добывается регуляркой), поэтому там
# предупреждение. Здесь сигнал точный — имя ТЧ берётся из атрибута, — а последствие жёсткое,
# поэтому ошибка.
# Корень путей формы — имя её основного реквизита: «Объект» только у формы объекта, у формы
# списка «Список», у формы записи регистра «Запись». С зашитым «Объект» обе проверки на
# таких формах молча не срабатывали. Ищем сначала в <Attributes> формы, потом в <BaseForm>.
root_match = MAIN_ATTR_RE.search(raw)
root_name = root_match.group(1) if root_match else ""
ac_tables = set()
if root_name:
ac_tables = set(re.findall(r'<AdditionalColumns table="' + re.escape(root_name) + r'\.(\w+)"', raw))
if ac_tables:
owner_key = ctx.split('.Form.')[0]
owner_ts = borrowed_ts_index.get(owner_key, {})
for tbl_name in sorted(ac_tables):
dep_check_count += 1
if tbl_name not in owner_ts:
r.error(f'12. {ctx}: <AdditionalColumns table="{root_name}.{tbl_name}"> — TabularSection.{tbl_name} not borrowed in extension')
check12_ok = False
for mi in missing_items: for mi in missing_items:
r.warn(f'12. {ctx}: references {mi} not borrowed in extension') r.warn(f'12. {ctx}: references {mi} not borrowed in extension')
check12_ok = False check12_ok = False
@@ -909,6 +1021,130 @@ def main():
elif check13_ok: elif check13_ok:
r.ok('13. TypeLink: clean') r.ok('13. TypeLink: clean')
# --- Check 14: пути Объект.* заимствованных форм против конфигурации-источника ---
# Требует -ConfigPath: отличить живой путь от висячего можно только по исходному объекту.
# «Объект.Партнер» валиден и без заимствования реквизита (наследуется от базы), а «Объект.Товары.Артикул»
# не разрешится нигде, если Артикул — не колонка ТЧ и не колонка из <Columns> самой формы.
# Такой путь платформа отвергает на загрузке: «Неверный путь к данным».
if not r.stopped and borrowed_forms_with_tree:
if not config_path_arg:
r.out('[INFO] 14. Пути Объект.* против конфигурации-источника не проверялись: не задан -ConfigPath')
else:
cfg_root = config_path_arg
if not os.path.isabs(cfg_root):
cfg_root = os.path.join(os.getcwd(), cfg_root)
if os.path.exists(cfg_root) and not os.path.isdir(cfg_root):
cfg_root = os.path.dirname(cfg_root)
if not os.path.isfile(os.path.join(cfg_root, 'Configuration.xml')):
r.warn(f"14. -ConfigPath '{config_path_arg}': Configuration.xml не найден — проверка путей пропущена")
else:
check14_ok = True
path_check_count = 0
for bf in borrowed_forms_with_tree:
raw = bf['RawText']
ctx = bf['Context']
# Корень путей — имя основного реквизита формы (см. проверку 12). Нет его ни в
# <Attributes> формы, ни в <BaseForm> — путей с корнем не бывает, проверять нечего.
root_match14 = MAIN_ATTR_RE.search(raw)
if root_match14 is None:
continue
root_name14 = root_match14.group(1)
# У динамического списка набор полей — результат его запроса, а не состав объекта:
# туда входят и стандартные поля списка (Ref, Date, DefaultPicture), и псевдонимы
# запроса. Сверять такие пути с ChildObjects объекта нельзя — будут ложные ошибки
# (корпусная проверка: 3383 таких сегмента на 1094 формах списка УТ).
if '>cfg:DynamicList<' in root_match14.group(0):
continue
owner_key = ctx.split('.Form.')[0]
owner_parts = owner_key.split('.', 1)
if len(owner_parts) < 2:
continue
owner_type, owner_name = owner_parts
owner_dir = CHILD_TYPE_DIR_MAP.get(owner_type)
if not owner_dir:
continue
src_obj_file = os.path.join(cfg_root, owner_dir, f'{owner_name}.xml')
if not os.path.isfile(src_obj_file):
r.warn(f'14. {ctx}: объект-источник не найден в конфигурации ({owner_dir}/{owner_name}.xml)')
continue
# Имена, доступные первым сегментом пути: реквизиты и ТЧ объекта-источника.
# Плюс для каждой ТЧ — её колонки: второй сегмент проверяем по ним (именно там
# и жил дефект — Объект.Товары.Артикул при живой ТЧ Товары).
src_names = set()
src_ts_columns = {}
src_tree = etree.parse(src_obj_file, etree.XMLParser(remove_blank_text=True))
src_obj_el = None
for c in src_tree.getroot():
if isinstance(c.tag, str):
src_obj_el = c
break
src_child_objects = src_obj_el.find(f'{{{MD}}}ChildObjects') if src_obj_el is not None else None
if src_child_objects is not None:
for sub in src_child_objects:
if not isinstance(sub.tag, str):
continue
sub_ln = etree.QName(sub.tag).localname
# У регистра дочерние объекты — Dimension/Resource, а не Attribute: без них
# замена корня превратила бы тихий пропуск в ложные ошибки на форме записи.
if sub_ln not in ('Attribute', 'Dimension', 'Resource', 'TabularSection'):
continue
name_el = sub.find(f'{{{MD}}}Properties/{{{MD}}}Name')
if name_el is None or not name_el.text:
continue
sub_name = name_el.text.strip()
src_names.add(sub_name)
if sub_ln != 'TabularSection':
continue
cols = set()
for col_name in sub.findall(f'{{{MD}}}ChildObjects/{{{MD}}}Attribute/{{{MD}}}Properties/{{{MD}}}Name'):
if col_name.text:
cols.add(col_name.text.strip())
src_ts_columns[sub_name] = cols
# Плюс колонки, объявленные в самой форме через <Columns>/<AdditionalColumns table="Объект.X">
root_pat14 = re.escape(root_name14)
for acm in re.finditer(r'<AdditionalColumns table="' + root_pat14 + r'\.(\w+)">(.*?)</AdditionalColumns>', raw, re.DOTALL):
tbl = acm.group(1)
cols = src_ts_columns.setdefault(tbl, set())
for cm in re.finditer(r'<Column name="(\w+)"', acm.group(2)):
cols.add(cm.group(1))
bad_paths = {}
for m in re.finditer(r'<(?:\w+:)?\w*DataPath[^>]*>' + root_pat14 + r'\.([^<]+)</(?:\w+:)?\w*DataPath>', raw):
segments = m.group(1).split('.')
seg0 = segments[0]
path_check_count += 1
if seg0 in STANDARD_OBJECT_FIELDS:
continue
if seg0 not in src_names:
bad_paths[f'{root_name14}.{seg0}'] = f'у {owner_key} нет такого реквизита или табличной части'
continue
# Второй сегмент проверяем только для табличных частей: у ссылочного реквизита
# он ведёт в чужой объект, и это уже другая проверка.
if len(segments) < 2 or seg0 not in src_ts_columns:
continue
seg1 = segments[1]
if seg1 in STANDARD_OBJECT_FIELDS:
continue
# Итог колонки — псевдополе платформы: Total<Колонка> при живой колонке законен
if seg1.startswith('Total') and seg1[5:] in src_ts_columns[seg0]:
continue
if seg1 not in src_ts_columns[seg0]:
bad_paths[f'{root_name14}.{seg0}.{seg1}'] = f'у табличной части {seg0} нет колонки {seg1}, и <Columns> формы её не объявляет'
for bad in sorted(bad_paths):
r.error(f"14. {ctx}: путь '{bad}'{bad_paths[bad]}")
check14_ok = False
if check14_ok:
r.ok(f'14. Object paths vs source config: {path_check_count} checked')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here --- # --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
ctrl_count = 0 ctrl_count = 0
for dp, _dn, files in os.walk(config_dir): for dp, _dn, files in os.walk(config_dir):
@@ -31,7 +31,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <параметры> python ".codex/skills/db-create/scripts/db-create.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -59,14 +59,14 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <п
```powershell ```powershell
# Создать файловую базу # Создать файловую базу
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" python ".codex/skills/db-create/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB"
# Создать серверную базу # Создать серверную базу
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" python ".codex/skills/db-create/scripts/db-create.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
# Создать из шаблона CF # Создать из шаблона CF
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" python ".codex/skills/db-create/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
# Создать и добавить в список баз # Создать и добавить в список баз
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база" python ".codex/skills/db-create/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
``` ```
@@ -35,7 +35,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <параметры> python ".codex/skills/db-dump-cf/scripts/db-dump-cf.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -60,11 +60,11 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <п
```powershell ```powershell
# Выгрузка конфигурации (файловая база) # Выгрузка конфигурации (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf" python ".codex/skills/db-dump-cf/scripts/db-dump-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf" python ".codex/skills/db-dump-cf/scripts/db-dump-cf.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
# Выгрузка расширения # Выгрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение" python ".codex/skills/db-dump-cf/scripts/db-dump-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
``` ```
@@ -38,7 +38,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <параметры> python ".codex/skills/db-dump-dt/scripts/db-dump-dt.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -61,10 +61,10 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <п
```powershell ```powershell
# Выгрузка ИБ (файловая база) # Выгрузка ИБ (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\base.dt" python ".codex/skills/db-dump-dt/scripts/db-dump-dt.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\base.dt"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "base.dt" python ".codex/skills/db-dump-dt/scripts/db-dump-dt.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "base.dt"
``` ```
## Связанные навыки ## Связанные навыки
@@ -37,7 +37,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <параметры> python ".codex/skills/db-dump-xml/scripts/db-dump-xml.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -76,17 +76,17 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <
```powershell ```powershell
# Полная выгрузка (файловая база) # Полная выгрузка (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full python ".codex/skills/db-dump-xml/scripts/db-dump-xml.py" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
# Инкрементальная выгрузка # Инкрементальная выгрузка
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes python ".codex/skills/db-dump-xml/scripts/db-dump-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
# Частичная выгрузка # Частичная выгрузка
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ" python ".codex/skills/db-dump-xml/scripts/db-dump-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full python ".codex/skills/db-dump-xml/scripts/db-dump-xml.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
# Выгрузка расширения # Выгрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение" python ".codex/skills/db-dump-xml/scripts/db-dump-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
``` ```
@@ -36,7 +36,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <параметры> python ".codex/skills/db-load-cf/scripts/db-load-cf.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -65,11 +65,11 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <п
```powershell ```powershell
# Файловая база # Файловая база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf" python ".codex/skills/db-load-cf/scripts/db-load-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf" python ".codex/skills/db-load-cf/scripts/db-load-cf.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
# Загрузка расширения # Загрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение" python ".codex/skills/db-load-cf/scripts/db-load-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
``` ```
@@ -52,7 +52,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <параметры> python ".codex/skills/db-load-dt/scripts/db-load-dt.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -82,10 +82,10 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <п
```powershell ```powershell
# Файловая база # Файловая база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\base.dt" python ".codex/skills/db-load-dt/scripts/db-load-dt.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\base.dt"
# Серверная база с ускорением загрузки # Серверная база с ускорением загрузки
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "base.dt" -JobsCount 4 python ".codex/skills/db-load-dt/scripts/db-load-dt.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "base.dt" -JobsCount 4
``` ```
## Связанные навыки ## Связанные навыки
@@ -38,7 +38,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <параметры> python ".codex/skills/db-load-git/scripts/db-load-git.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -72,8 +72,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <
```powershell ```powershell
# Все незафиксированные изменения # Все незафиксированные изменения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB python ".codex/skills/db-load-git/scripts/db-load-git.py" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB
# Из диапазона коммитов # Из диапазона коммитов
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD" python ".codex/skills/db-load-git/scripts/db-load-git.py" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD"
``` ```
@@ -1,4 +1,4 @@
# db-load-git v1.19 — Load Git changes into 1C database # db-load-git v1.20 — Load Git changes into 1C database
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -110,6 +110,12 @@ param(
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[switch]$UpdateDB, [switch]$UpdateDB,
[Parameter(Mandatory=$false)]
# Ключ для регрессов и верификации снапшотов, не для повседневного вызова: в SKILL.md
# намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе,
# но в логе есть отбраковка.
[switch]$StrictLog,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(), [string[]]$AdditionalV8Arguments = @(),
@@ -394,6 +400,41 @@ function Write-PlatformOutput {
Write-Host "--- End ---" Write-Host "--- End ---"
} }
# Строки лога, о которых платформа сообщает, НЕ поднимая код возврата: метаданные отброшены или
# конфигурация нерабочая, а операция при этом «успешна». Возвращает подошедшие строки.
#
# Копия этой функции есть в каждом навыке, который читает /Out-лог загрузки (навыки автономны).
# Держать копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
function Find-SilentRejections {
param([string]$LogText)
$patterns = @(
'Неверное свойство объекта метаданных',
'не входит в состав объекта метаданных',
'Неизвестное имя типа',
'Неизвестный объект метаданных',
'Ни один из документов не является регистратором для регистра',
'Неверное значение перечисления',
'не может быть приведен к типу',
# Режим совместимости выше платформы: объекты в базу не попадают, отказ приходит в рантайме.
# Обрезано до инвариантной части — конкретная версия в сообщении меняется.
'Для работы с конфигурацией необходима версия платформы не меньше'
)
$found = @()
if ($LogText) {
foreach ($line in ($LogText -split "`r?`n")) {
foreach ($pat in $patterns) {
if ($line -match [regex]::Escape($pat)) {
$found += $line.Trim()
break
}
}
}
}
# Возвращаем массив БЕЗ запятой-обёртки: вызывающий берёт результат в @(), а `return ,$found`
# дал бы массив из одного пустого массива — фантомное срабатывание на чистом логе.
return $found
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
if ($engine -eq "ibcmd") { if ($engine -eq "ibcmd") {
@@ -667,6 +708,7 @@ try {
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
} }
$logContent = $null
if (Test-Path $outFile) { if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) { if ($logContent) {
@@ -677,6 +719,16 @@ try {
} }
Write-PlatformOutput $__v8.Output Write-PlatformOutput $__v8.Output
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
# про -StrictLog не даём: операция уже выполнена, повторять её ради того же текста незачем.
$silentFailures = @(Find-SilentRejections $logContent)
if ($silentFailures.Count -gt 0) {
Write-Host "[warning] platform reported success, but the log contains $($silentFailures.Count) problem(s):" -ForegroundColor Yellow
foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow }
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
}
exit $exitCode exit $exitCode
} finally { } finally {
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-git v1.19 — Load Git changes into 1C database # db-load-git v1.20 — Load Git changes into 1C database
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -344,6 +344,38 @@ def print_platform_output(result):
print("--- End ---") print("--- End ---")
def find_silent_rejections(log_text):
"""Строки лога, о которых платформа сообщает, НЕ поднимая код возврата.
Метаданные отброшены или конфигурация нерабочая, а операция при этом «успешна».
Возвращает подошедшие строки.
Копия этой функции есть в каждом навыке, который читает /Out-лог загрузки (навыки
автономны). Держать копии одинаковыми сознательно: разошедшиеся копии сводят на нет
весь смысл.
"""
patterns = [
"Неверное свойство объекта метаданных",
"не входит в состав объекта метаданных",
"Неизвестное имя типа",
"Неизвестный объект метаданных",
"Ни один из документов не является регистратором для регистра",
"Неверное значение перечисления",
"не может быть приведен к типу",
# Режим совместимости выше платформы: объекты в базу не попадают, отказ приходит в
# рантайме. Обрезано до инвариантной части — конкретная версия в сообщении меняется.
"Для работы с конфигурацией необходима версия платформы не меньше",
]
found = []
if log_text:
for line in log_text.splitlines():
for pat in patterns:
if pat in line:
found.append(line.strip())
break
return found
def run_ibcmd(cmd, has_username=False, warn_no_user=True): def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively. """Run an ibcmd command non-interactively.
@@ -446,6 +478,10 @@ def main():
) )
parser.add_argument("-DryRun", action="store_true", help="Only show what would be loaded (no actual load)") parser.add_argument("-DryRun", action="store_true", help="Only show what would be loaded (no actual load)")
parser.add_argument("-UpdateDB", action="store_true", help="Also update database configuration after load") parser.add_argument("-UpdateDB", action="store_true", help="Also update database configuration after load")
# Ключ для регрессов и верификации снапшотов, не для повседневного вызова: в SKILL.md
# намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе,
# но в логе есть отбраковка.
parser.add_argument("-StrictLog", action="store_true")
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[], parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+") help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[], parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
@@ -705,6 +741,7 @@ def main():
else: else:
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr) print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
log_content = ""
if os.path.isfile(out_file): if os.path.isfile(out_file):
try: try:
with open(out_file, "r", encoding="utf-8-sig") as f: with open(out_file, "r", encoding="utf-8-sig") as f:
@@ -717,6 +754,21 @@ def main():
pass pass
print_platform_output(result) print_platform_output(result)
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
# про -StrictLog не даём: операция уже выполнена, повторять её ради того же текста незачем.
silent_failures = find_silent_rejections(log_content)
if silent_failures:
print(
f"[warning] platform reported success, but the log contains "
f"{len(silent_failures)} problem(s):"
)
for line in silent_failures:
print(f" {line}")
if args.StrictLog and exit_code == 0:
exit_code = 1
sys.exit(exit_code) sys.exit(exit_code)
finally: finally:
@@ -38,7 +38,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" <параметры> python ".codex/skills/db-load-xml/scripts/db-load-xml.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -90,14 +90,14 @@ Documents/Заказ/Forms/ФормаДокумента.xml
```powershell ```powershell
# Полная загрузка # Полная загрузка
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full python ".codex/skills/db-load-xml/scripts/db-load-xml.py" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
# Частичная загрузка конкретных файлов # Частичная загрузка конкретных файлов
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl" python ".codex/skills/db-load-xml/scripts/db-load-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
# Загрузка расширения # Загрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение" python ".codex/skills/db-load-xml/scripts/db-load-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
# Загрузка + обновление БД в одном запуске # Загрузка + обновление БД в одном запуске
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB python ".codex/skills/db-load-xml/scripts/db-load-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
``` ```
@@ -1,4 +1,4 @@
# db-load-xml v1.20 — Load 1C configuration from XML files # db-load-xml v1.21 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -416,6 +416,41 @@ function Write-PlatformOutput {
Write-Host "--- End ---" Write-Host "--- End ---"
} }
# Строки лога, о которых платформа сообщает, НЕ поднимая код возврата: метаданные отброшены или
# конфигурация нерабочая, а операция при этом «успешна». Возвращает подошедшие строки.
#
# Копия этой функции есть в каждом навыке, который читает /Out-лог загрузки (навыки автономны).
# Держать копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
function Find-SilentRejections {
param([string]$LogText)
$patterns = @(
'Неверное свойство объекта метаданных',
'не входит в состав объекта метаданных',
'Неизвестное имя типа',
'Неизвестный объект метаданных',
'Ни один из документов не является регистратором для регистра',
'Неверное значение перечисления',
'не может быть приведен к типу',
# Режим совместимости выше платформы: объекты в базу не попадают, отказ приходит в рантайме.
# Обрезано до инвариантной части — конкретная версия в сообщении меняется.
'Для работы с конфигурацией необходима версия платформы не меньше'
)
$found = @()
if ($LogText) {
foreach ($line in ($LogText -split "`r?`n")) {
foreach ($pat in $patterns) {
if ($line -match [regex]::Escape($pat)) {
$found += $line.Trim()
break
}
}
}
}
# Возвращаем массив БЕЗ запятой-обёртки: вызывающий берёт результат в @(), а `return ,$found`
# дал бы массив из одного пустого массива — фантомное срабатывание на чистом логе.
return $found
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
@@ -607,28 +642,7 @@ try {
} }
# --- Scan log for silent rejections --- # --- Scan log for silent rejections ---
# Platform often writes load-time rejections into /Out but exits with code 0. $silentFailures = @(Find-SilentRejections $logContent)
# These patterns flag cases where metadata was dropped or rejected silently.
$fatalLogPatterns = @(
'Неверное свойство объекта метаданных',
'не входит в состав объекта метаданных',
'Неизвестное имя типа',
'Неизвестный объект метаданных',
'Ни один из документов не является регистратором для регистра',
'Неверное значение перечисления',
'не может быть приведен к типу'
)
$silentFailures = @()
if ($logContent) {
foreach ($line in ($logContent -split "`r?`n")) {
foreach ($pat in $fatalLogPatterns) {
if ($line -match [regex]::Escape($pat)) {
$silentFailures += $line.Trim()
break
}
}
}
}
# --- Result --- # --- Result ---
# Default: mirror platform's verdict via exit code. Log content (including any # Default: mirror platform's verdict via exit code. Log content (including any
@@ -647,10 +661,11 @@ try {
} }
Write-PlatformOutput $__v8.Output Write-PlatformOutput $__v8.Output
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
# про -StrictLog не даём: загрузка уже выполнена, повторять её ради того же текста незачем.
if ($silentFailures.Count -gt 0) { if ($silentFailures.Count -gt 0) {
$msg = "[warning] log contains $($silentFailures.Count) rejection(s) — platform loaded config but dropped properties/refs" Write-Host "[warning] platform reported success, but the log contains $($silentFailures.Count) problem(s):" -ForegroundColor Yellow
if (-not $StrictLog) { $msg += " (pass -StrictLog to treat as error)" }
Write-Host $msg -ForegroundColor Yellow
foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow } foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow }
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 } if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
} }
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-xml v1.20 — Load 1C configuration from XML files # db-load-xml v1.21 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -344,6 +344,38 @@ def print_platform_output(result):
print("--- End ---") print("--- End ---")
def find_silent_rejections(log_text):
"""Строки лога, о которых платформа сообщает, НЕ поднимая код возврата.
Метаданные отброшены или конфигурация нерабочая, а операция при этом «успешна».
Возвращает подошедшие строки.
Копия этой функции есть в каждом навыке, который читает /Out-лог загрузки (навыки
автономны). Держать копии одинаковыми сознательно: разошедшиеся копии сводят на нет
весь смысл.
"""
patterns = [
"Неверное свойство объекта метаданных",
"не входит в состав объекта метаданных",
"Неизвестное имя типа",
"Неизвестный объект метаданных",
"Ни один из документов не является регистратором для регистра",
"Неверное значение перечисления",
"не может быть приведен к типу",
# Режим совместимости выше платформы: объекты в базу не попадают, отказ приходит в
# рантайме. Обрезано до инвариантной части — конкретная версия в сообщении меняется.
"Для работы с конфигурацией необходима версия платформы не меньше",
]
found = []
if log_text:
for line in log_text.splitlines():
for pat in patterns:
if pat in line:
found.append(line.strip())
break
return found
def run_ibcmd(cmd, has_username=False, warn_no_user=True): def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively. """Run an ibcmd command non-interactively.
@@ -636,22 +668,7 @@ def main():
# --- Scan log for silent rejections --- # --- Scan log for silent rejections ---
# Platform often writes load-time rejections into /Out but exits with code 0. # Platform often writes load-time rejections into /Out but exits with code 0.
# These patterns flag cases where metadata was dropped or rejected silently. # These patterns flag cases where metadata was dropped or rejected silently.
fatal_log_patterns = [ silent_failures = find_silent_rejections(log_content)
"Неверное свойство объекта метаданных",
"не входит в состав объекта метаданных",
"Неизвестное имя типа",
"Неизвестный объект метаданных",
"Ни один из документов не является регистратором для регистра",
"Неверное значение перечисления",
"не может быть приведен к типу",
]
silent_failures = []
if log_content:
for line in log_content.splitlines():
for pat in fatal_log_patterns:
if pat in line:
silent_failures.append(line.strip())
break
# --- Result --- # --- Result ---
# Default: mirror platform's verdict via exit code. Log content (including any # Default: mirror platform's verdict via exit code. Log content (including any
@@ -668,15 +685,19 @@ def main():
print("--- End ---") print("--- End ---")
print_platform_output(result) print_platform_output(result)
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
# про -StrictLog не даём: загрузка уже выполнена, повторять её ради того же текста незачем.
# Поток — stdout, как у PS1-порта: предупреждение относится к содержимому загрузки, а не к
# отказу навыка, и при code 0 остаётся предупреждением. Раньше py писал его в stderr —
# наблюдаемое поведение портов расходилось, и один кейс не мог проверить оба.
if silent_failures: if silent_failures:
suffix = "" if args.StrictLog else " (pass -StrictLog to treat as error)"
print( print(
f"[warning] log contains {len(silent_failures)} rejection(s) — " f"[warning] platform reported success, but the log contains "
f"platform loaded config but dropped properties/refs{suffix}", f"{len(silent_failures)} problem(s):"
file=sys.stderr,
) )
for f in silent_failures: for f in silent_failures:
print(f" {f}", file=sys.stderr) print(f" {f}")
if args.StrictLog and exit_code == 0: if args.StrictLog and exit_code == 0:
exit_code = 1 exit_code = 1
@@ -36,7 +36,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <параметры> python ".codex/skills/db-run/scripts/db-run.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -64,14 +64,14 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <пар
```powershell ```powershell
# Простой запуск # Простой запуск
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" python ".codex/skills/db-run/scripts/db-run.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
# Запуск с обработкой # Запуск с обработкой
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf" python ".codex/skills/db-run/scripts/db-run.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf"
# Открыть по навигационной ссылке # Открыть по навигационной ссылке
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура" python ".codex/skills/db-run/scripts/db-run.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура"
# Серверная база с параметром запуска # Серверная база с параметром запуска
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление" python ".codex/skills/db-run/scripts/db-run.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление"
``` ```
@@ -35,7 +35,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <параметры> python ".codex/skills/db-update/scripts/db-update.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -78,11 +78,11 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <п
```powershell ```powershell
# Обычное обновление (файловая база) # Обычное обновление (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" python ".codex/skills/db-update/scripts/db-update.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
# Динамическое обновление (серверная база) # Динамическое обновление (серверная база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+" python ".codex/skills/db-update/scripts/db-update.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
# Обновление расширения # Обновление расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение" python ".codex/skills/db-update/scripts/db-update.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
``` ```
@@ -1,4 +1,4 @@
# db-update v1.14 — Update 1C database configuration # db-update v1.15 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<# <#
@@ -91,6 +91,12 @@ param(
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[switch]$WarningsAsErrors, [switch]$WarningsAsErrors,
[Parameter(Mandatory=$false)]
# Ключ для регрессов и верификации снапшотов, не для повседневного вызова: в SKILL.md
# намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе,
# но в логе есть отбраковка.
[switch]$StrictLog,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(), [string[]]$AdditionalV8Arguments = @(),
@@ -395,6 +401,41 @@ function Write-PlatformOutput {
Write-Host "--- End ---" Write-Host "--- End ---"
} }
# Строки лога, о которых платформа сообщает, НЕ поднимая код возврата: метаданные отброшены или
# конфигурация нерабочая, а операция при этом «успешна». Возвращает подошедшие строки.
#
# Копия этой функции есть в каждом навыке, который читает /Out-лог загрузки (навыки автономны).
# Держать копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
function Find-SilentRejections {
param([string]$LogText)
$patterns = @(
'Неверное свойство объекта метаданных',
'не входит в состав объекта метаданных',
'Неизвестное имя типа',
'Неизвестный объект метаданных',
'Ни один из документов не является регистратором для регистра',
'Неверное значение перечисления',
'не может быть приведен к типу',
# Режим совместимости выше платформы: объекты в базу не попадают, отказ приходит в рантайме.
# Обрезано до инвариантной части — конкретная версия в сообщении меняется.
'Для работы с конфигурацией необходима версия платформы не меньше'
)
$found = @()
if ($LogText) {
foreach ($line in ($LogText -split "`r?`n")) {
foreach ($pat in $patterns) {
if ($line -match [regex]::Escape($pat)) {
$found += $line.Trim()
break
}
}
}
}
# Возвращаем массив БЕЗ запятой-обёртки: вызывающий берёт результат в @(), а `return ,$found`
# дал бы массив из одного пустого массива — фантомное срабатывание на чистом логе.
return $found
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" } $engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
@@ -496,6 +537,7 @@ try {
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
} }
$logContent = $null
if (Test-Path $outFile) { if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue $logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) { if ($logContent) {
@@ -506,6 +548,16 @@ try {
} }
Write-PlatformOutput $__v8.Output Write-PlatformOutput $__v8.Output
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
# про -StrictLog не даём: операция уже выполнена, повторять её ради того же текста незачем.
$silentFailures = @(Find-SilentRejections $logContent)
if ($silentFailures.Count -gt 0) {
Write-Host "[warning] platform reported success, but the log contains $($silentFailures.Count) problem(s):" -ForegroundColor Yellow
foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow }
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
}
exit $exitCode exit $exitCode
} finally { } finally {
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-update v1.14 — Update 1C database configuration # db-update v1.15 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -344,6 +344,38 @@ def print_platform_output(result):
print("--- End ---") print("--- End ---")
def find_silent_rejections(log_text):
"""Строки лога, о которых платформа сообщает, НЕ поднимая код возврата.
Метаданные отброшены или конфигурация нерабочая, а операция при этом «успешна».
Возвращает подошедшие строки.
Копия этой функции есть в каждом навыке, который читает /Out-лог загрузки (навыки
автономны). Держать копии одинаковыми сознательно: разошедшиеся копии сводят на нет
весь смысл.
"""
patterns = [
"Неверное свойство объекта метаданных",
"не входит в состав объекта метаданных",
"Неизвестное имя типа",
"Неизвестный объект метаданных",
"Ни один из документов не является регистратором для регистра",
"Неверное значение перечисления",
"не может быть приведен к типу",
# Режим совместимости выше платформы: объекты в базу не попадают, отказ приходит в
# рантайме. Обрезано до инвариантной части — конкретная версия в сообщении меняется.
"Для работы с конфигурацией необходима версия платформы не меньше",
]
found = []
if log_text:
for line in log_text.splitlines():
for pat in patterns:
if pat in line:
found.append(line.strip())
break
return found
def run_ibcmd(cmd, has_username=False, warn_no_user=True): def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively. """Run an ibcmd command non-interactively.
@@ -411,6 +443,10 @@ def main():
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"]) parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
parser.add_argument("-Server", action="store_true") parser.add_argument("-Server", action="store_true")
parser.add_argument("-WarningsAsErrors", action="store_true") parser.add_argument("-WarningsAsErrors", action="store_true")
# Ключ для регрессов и верификации снапшотов, не для повседневного вызова: в SKILL.md
# намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе,
# но в логе есть отбраковка.
parser.add_argument("-StrictLog", action="store_true")
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[], parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+") help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[], parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
@@ -527,6 +563,7 @@ def main():
else: else:
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr) print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
log_content = ""
if os.path.isfile(out_file): if os.path.isfile(out_file):
try: try:
with open(out_file, "r", encoding="utf-8-sig") as f: with open(out_file, "r", encoding="utf-8-sig") as f:
@@ -539,6 +576,21 @@ def main():
pass pass
print_platform_output(result) print_platform_output(result)
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
# про -StrictLog не даём: операция уже выполнена, повторять её ради того же текста незачем.
silent_failures = find_silent_rejections(log_content)
if silent_failures:
print(
f"[warning] platform reported success, but the log contains "
f"{len(silent_failures)} problem(s):"
)
for line in silent_failures:
print(f" {line}")
if args.StrictLog and exit_code == 0:
exit_code = 1
sys.exit(exit_code) sys.exit(exit_code)
finally: finally:
@@ -40,7 +40,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <параметры> python ".codex/skills/epf-build/scripts/epf-build.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -64,8 +64,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <п
```powershell ```powershell
# Сборка обработки (файловая база) # Сборка обработки (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf" python ".codex/skills/epf-build/scripts/epf-build.py" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf" python ".codex/skills/epf-build/scripts/epf-build.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
``` ```
@@ -1,4 +1,4 @@
# stub-db-create v1.7 — Create temp 1C infobase with metadata stubs for EPF/ERF build # stub-db-create v1.8 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -163,14 +163,35 @@ function Format-ArgsForDisplay {
} }
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
# --- 1. Scan XML files for reference types --- # --- 1. Scan XML files for reference types ---
$typeMap = @{} # MetadataType -> @(Name1, Name2, ...) $typeMap = @{} # MetadataType -> @(Name1, Name2, ...)
# Версия формата заглушечной конфигурации. Платформа грузит формат не новее себя, поэтому зашитая
# версия ломала бы сборку исходников более старого формата на соответствующей ей платформе. Берём
# версию из корня собираемого объекта (ExternalDataProcessor/ExternalReport); вложенные файлы —
# запасной вариант, если корень почему-то не попался.
$srcRootVersion = ""
$srcAnyVersion = ""
$xmlFiles = Get-ChildItem -Path $SourceDir -Filter "*.xml" -Recurse -File $xmlFiles = Get-ChildItem -Path $SourceDir -Filter "*.xml" -Recurse -File
foreach ($f in $xmlFiles) { foreach ($f in $xmlFiles) {
$content = [System.IO.File]::ReadAllText($f.FullName, [System.Text.Encoding]::UTF8) $content = [System.IO.File]::ReadAllText($f.FullName, [System.Text.Encoding]::UTF8)
if ($content -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') {
$ver = $Matches[1]
if (-not $srcAnyVersion) { $srcAnyVersion = $ver }
if (-not $srcRootVersion -and $content -match '<(ExternalDataProcessor|ExternalReport)[ >]') {
$srcRootVersion = $ver
}
}
# Ref types: cfg:CatalogRef.XXX or d5p1:CatalogRef.XXX (and similar depth prefixes d4p1, d3p1, etc.) # Ref types: cfg:CatalogRef.XXX or d5p1:CatalogRef.XXX (and similar depth prefixes d4p1, d3p1, etc.)
$refPattern = '(?:cfg:|d\dp1:)(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.([A-Za-z\u0400-\u04FF\d_]+)' $refPattern = '(?:cfg:|d\dp1:)(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef)\.([A-Za-z\u0400-\u04FF\d_]+)'
foreach ($m in [regex]::Matches($content, $refPattern)) { foreach ($m in [regex]::Matches($content, $refPattern)) {
@@ -337,7 +358,24 @@ if ($hasRefTypes) {
$cfgDir = Join-Path $TempBasePath "cfg" $cfgDir = Join-Path $TempBasePath "cfg"
New-Item -ItemType Directory -Path $cfgDir -Force | Out-Null New-Item -ItemType Directory -Path $cfgDir -Force | Out-Null
$ns = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17"' # Заглушке нужна САМАЯ НИЗКАЯ работающая версия, а не версия исходников: ограничение платформы
# одностороннее — она читает формат не новее себя. Отсюда min(версия исходников, 2.17): на 2.17+
# заглушка остаётся 2.17 (как было), а под исходники 2.13-2.16 опускается до их версии, иначе
# конфигурация не загрузится платформой, которая эти исходники и выгрузила («Неизвестная версия
# формата 2.17 загружаемого файла», замерено на 8.3.20).
#
$srcVersion = if ($srcRootVersion) { $srcRootVersion } elseif ($srcAnyVersion) { $srcAnyVersion } else { "2.17" }
$srcRank = Get-FormatRank $srcVersion
$stubFormatVersion = if ($srcRank -gt 0 -and $srcRank -lt (Get-FormatRank "2.17")) { $srcVersion } else { "2.17" }
# Режим совместимости заглушки — по той же логике. Платформа отказывается работать с
# конфигурацией, чей режим выше её самой («Для работы с конфигурацией необходима версия
# платформы не меньше, чем 8.3.24»), и тогда объекты заглушки в базу не попадают: загрузка
# рапортует успех, а сборка падает на «Неизвестное имя типа». Ступени — лестница версий
# формата из docs/1c-configuration-spec.md.
$compatByFormat = @{ "2.13" = "Version8_3_20"; "2.14" = "Version8_3_21"; "2.15" = "Version8_3_22"; "2.16" = "Version8_3_23" }
$stubCompatMode = if ($compatByFormat.ContainsKey($stubFormatVersion)) { $compatByFormat[$stubFormatVersion] } else { "Version8_3_24" }
$ns = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="' + $stubFormatVersion + '"'
# GeneratedType definitions per metadata type # GeneratedType definitions per metadata type
$gtDefs = @{ $gtDefs = @{
@@ -521,7 +559,7 @@ if ($hasRefTypes) {
<Synonym/> <Synonym/>
<Comment/> <Comment/>
<NamePrefix/> <NamePrefix/>
<ConfigurationExtensionCompatibilityMode>Version8_3_24</ConfigurationExtensionCompatibilityMode> <ConfigurationExtensionCompatibilityMode>$stubCompatMode</ConfigurationExtensionCompatibilityMode>
<DefaultRunMode>ManagedApplication</DefaultRunMode> <DefaultRunMode>ManagedApplication</DefaultRunMode>
<UsePurposes> <UsePurposes>
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value> <v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
@@ -572,7 +610,7 @@ if ($hasRefTypes) {
<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode> <SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
<InterfaceCompatibilityMode>Taxi</InterfaceCompatibilityMode> <InterfaceCompatibilityMode>Taxi</InterfaceCompatibilityMode>
<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode> <DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
<CompatibilityMode>Version8_3_24</CompatibilityMode> <CompatibilityMode>$stubCompatMode</CompatibilityMode>
<DefaultConstantsForm/> <DefaultConstantsForm/>
</Properties> </Properties>
<ChildObjects>$childXml <ChildObjects>$childXml
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# stub-db-create v1.7 — Create temp 1C infobase with metadata stubs for EPF/ERF build # stub-db-create v1.8 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -339,6 +339,66 @@ def scan_ref_types(source_dir):
return type_map return type_map
def format_rank(ver):
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
def detect_stub_format_version(source_dir):
"""Версия формата заглушечной конфигурации.
Заглушке нужна САМАЯ НИЗКАЯ работающая версия, а не версия исходников: ограничение платформы
одностороннее она читает формат не новее себя. Отсюда min(версия исходников, 2.17): на 2.17+
заглушка остаётся 2.17 (как было), а под исходники 2.13-2.16 опускается до их версии, иначе
конфигурация не загрузится платформой, которая эти исходники и выгрузила («Неизвестная версия
формата 2.17 загружаемого файла», замерено на 8.3.20).
Версию исходников берём из корня собираемого объекта (ExternalDataProcessor/ExternalReport);
вложенные файлы запасной вариант, если корень почему-то не попался.
"""
root_version = ""
any_version = ""
ver_pattern = re.compile(r'<MetaDataObject[^>]+version="(\d+\.\d+)"')
root_pattern = re.compile(r'<(ExternalDataProcessor|ExternalReport)[ >]')
for dirpath, _, filenames in os.walk(source_dir):
for fn in filenames:
if not fn.endswith('.xml'):
continue
try:
with open(os.path.join(dirpath, fn), 'r', encoding='utf-8-sig') as f:
content = f.read()
except Exception:
continue
m = ver_pattern.search(content)
if not m:
continue
if not any_version:
any_version = m.group(1)
if not root_version and root_pattern.search(content):
root_version = m.group(1)
src_version = root_version or any_version or "2.17"
src_rank = format_rank(src_version)
return src_version if 0 < src_rank < format_rank("2.17") else "2.17"
# Режим совместимости заглушки — по той же логике, что и версия формата. Платформа отказывается
# работать с конфигурацией, чей режим выше её самой («Для работы с конфигурацией необходима версия
# платформы не меньше, чем 8.3.24»), и тогда объекты заглушки в базу не попадают: загрузка
# рапортует успех, а сборка падает на «Неизвестное имя типа». Ступени — лестница версий формата
# из docs/1c-configuration-spec.md.
COMPAT_BY_FORMAT = {
"2.13": "Version8_3_20",
"2.14": "Version8_3_21",
"2.15": "Version8_3_22",
"2.16": "Version8_3_23",
}
def stub_compatibility_mode(format_version):
return COMPAT_BY_FORMAT.get(format_version, "Version8_3_24")
def scan_register_columns(source_dir): def scan_register_columns(source_dir):
"""Scan Form.xml for register record set columns referenced via DataPath. """Scan Form.xml for register record set columns referenced via DataPath.
Returns {"RegisterType.RegisterName": {"col1": True, "col2": True}}.""" Returns {"RegisterType.RegisterName": {"col1": True, "col2": True}}."""
@@ -417,7 +477,7 @@ NS = (
'xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" ' 'xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" '
'xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" ' 'xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" '
'xmlns:xs="http://www.w3.org/2001/XMLSchema" ' 'xmlns:xs="http://www.w3.org/2001/XMLSchema" '
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17"' 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
) )
CLASS_IDS = [ CLASS_IDS = [
@@ -1046,6 +1106,9 @@ def main():
type_map = scan_ref_types(args.SourceDir) type_map = scan_ref_types(args.SourceDir)
register_columns = scan_register_columns(args.SourceDir) register_columns = scan_register_columns(args.SourceDir)
has_ref_types = len(type_map) > 0 has_ref_types = len(type_map) > 0
stub_format_version = detect_stub_format_version(args.SourceDir)
stub_compat = stub_compatibility_mode(stub_format_version)
ns_decl = f'{NS} version="{stub_format_version}"'
temp_base = args.TempBasePath or os.path.join(tempfile.gettempdir(), f'epf_stub_db_{random.randint(0,999999)}') temp_base = args.TempBasePath or os.path.join(tempfile.gettempdir(), f'epf_stub_db_{random.randint(0,999999)}')
@@ -1077,7 +1140,7 @@ def main():
child_xml += f'\n\t\t\t<{tag}>{name}</{tag}>' child_xml += f'\n\t\t\t<{tag}>{name}</{tag}>'
cfg_xml = f"""<?xml version="1.0" encoding="UTF-8"?> cfg_xml = f"""<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject {NS}> <MetaDataObject {ns_decl}>
\t<Configuration uuid="{uuid_cfg}"> \t<Configuration uuid="{uuid_cfg}">
\t\t<InternalInfo>{co_xml} \t\t<InternalInfo>{co_xml}
\t\t</InternalInfo> \t\t</InternalInfo>
@@ -1086,7 +1149,7 @@ def main():
\t\t\t<Synonym/> \t\t\t<Synonym/>
\t\t\t<Comment/> \t\t\t<Comment/>
\t\t\t<NamePrefix/> \t\t\t<NamePrefix/>
\t\t\t<ConfigurationExtensionCompatibilityMode>Version8_3_24</ConfigurationExtensionCompatibilityMode> \t\t\t<ConfigurationExtensionCompatibilityMode>{stub_compat}</ConfigurationExtensionCompatibilityMode>
\t\t\t<DefaultRunMode>ManagedApplication</DefaultRunMode> \t\t\t<DefaultRunMode>ManagedApplication</DefaultRunMode>
\t\t\t<UsePurposes> \t\t\t<UsePurposes>
\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value> \t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
@@ -1137,7 +1200,7 @@ def main():
\t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode> \t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
\t\t\t<InterfaceCompatibilityMode>Taxi</InterfaceCompatibilityMode> \t\t\t<InterfaceCompatibilityMode>Taxi</InterfaceCompatibilityMode>
\t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode> \t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
\t\t\t<CompatibilityMode>Version8_3_24</CompatibilityMode> \t\t\t<CompatibilityMode>{stub_compat}</CompatibilityMode>
\t\t\t<DefaultConstantsForm/> \t\t\t<DefaultConstantsForm/>
\t\t</Properties> \t\t</Properties>
\t\t<ChildObjects>{child_xml} \t\t<ChildObjects>{child_xml}
@@ -1151,7 +1214,7 @@ def main():
lang_dir = os.path.join(cfg_dir, 'Languages') lang_dir = os.path.join(cfg_dir, 'Languages')
os.makedirs(lang_dir, exist_ok=True) os.makedirs(lang_dir, exist_ok=True)
lang_xml = f"""<?xml version="1.0" encoding="UTF-8"?> lang_xml = f"""<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject {NS}> <MetaDataObject {ns_decl}>
\t<Language uuid="{uuid_lang}"> \t<Language uuid="{uuid_lang}">
\t\t<Properties> \t\t<Properties>
\t\t\t<Name>\u0420\u0443\u0441\u0441\u043a\u0438\u0439</Name> \t\t\t<Name>\u0420\u0443\u0441\u0441\u043a\u0438\u0439</Name>
@@ -1280,7 +1343,7 @@ def main():
child_obj_xml = '\n\t\t<ChildObjects/>' child_obj_xml = '\n\t\t<ChildObjects/>'
obj_xml = f"""<?xml version="1.0" encoding="UTF-8"?> obj_xml = f"""<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject {NS}> <MetaDataObject {ns_decl}>
\t<{tag} uuid="{obj_uuid}">{internal_xml} \t<{tag} uuid="{obj_uuid}">{internal_xml}
\t\t<Properties> \t\t<Properties>
{props_xml} {props_xml}
@@ -39,7 +39,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <параметры> python ".codex/skills/epf-dump/scripts/epf-dump.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -64,8 +64,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <па
```powershell ```powershell
# Разборка обработки (файловая база) # Разборка обработки (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src" python ".codex/skills/epf-dump/scripts/epf-dump.py" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src" python ".codex/skills/epf-dump/scripts/epf-dump.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src"
``` ```
@@ -26,12 +26,18 @@ allowed-tools:
| Name | да | — | Имя обработки (латиница/кириллица) | | Name | да | — | Имя обработки (латиница/кириллица) |
| Synonym | нет | = Name | Синоним (отображаемое имя) | | Synonym | нет | = Name | Синоним (отображаемое имя) |
| SrcDir | нет | `src` | Каталог исходников относительно CWD | | SrcDir | нет | `src` | Каталог исходников относительно CWD |
| FormatVersion | нет | `2.17` | Версия формата: 2.20 — платформа 8.3.27, 2.21 — 8.5. Дефолт открывается любой платформой | | FormatVersion | нет | `2.17` | Версия формата выгрузки — см. ниже |
`FormatVersion`**не выше** версии формата платформы, на которой объект будут собирать и открывать:
8.3.24 — `2.17`, 8.3.25 — `2.18`, 8.3.26 — `2.19`, 8.3.27 — `2.20`, 8.5 — `2.21`. Ниже брать можно:
платформа читает свой формат и любой более старый, поэтому дефолт `2.17` подходит для всей линейки
8.3.24 и выше. Для более старых платформ счёт идёт так же, по одной версии на релиз (8.3.23 — `2.16`),
но на них навыки не проверялись — такое значение принимается с предупреждением.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-FormatVersion "<2.17|2.18|2.19|2.20|2.21>"] python ".codex/skills/epf-init/scripts/init.py" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-FormatVersion "<версия>"]
``` ```
## Дальнейшие шаги ## Дальнейшие шаги
@@ -1,4 +1,4 @@
# epf-init v1.7 — Init 1C external data processor scaffold (+write_xml_file/write_utf8_bom: общий эталон записи) # epf-init v1.8 — Init 1C external data processor scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -11,12 +11,25 @@ param(
# Версия формата выгрузки. Своей конфигурации у автономной обработки нет, наследовать # Версия формата выгрузки. Своей конфигурации у автономной обработки нет, наследовать
# версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри обработки # версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри обработки
# навыки берут уже отсюда: их детектор читает version из корня этого файла. # навыки берут уже отсюда: их детектор читает version из корня этого файла.
[ValidateSet("2.17", "2.18", "2.19", "2.20", "2.21")]
[string]$FormatVersion = "2.17" [string]$FormatVersion = "2.17"
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
# --- Format version ---
# Проверенный диапазон: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница — docs/1c-configuration-spec.md,
# «Лестница версий». Версии ниже 2.17 (платформы 8.3.23 и старше) реальны, поэтому запретом их не
# закрываем: за пределами диапазона — ПРЕДУПРЕЖДЕНИЕ, скаффолд всё равно выпускается. Ошибка — только
# на нечисловое значение: это опечатка, а не версия.
$formatVerifiedMin = "2.17"
$formatVerifiedMax = "2.21"
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
$formatRank = Get-FormatRank $FormatVersion
function Esc-XmlText { function Esc-XmlText {
param([string]$s) param([string]$s)
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми. # Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
@@ -25,6 +38,17 @@ function Esc-XmlText {
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8 [Console]::InputEncoding = [System.Text.Encoding]::UTF8
# Проверка версии — ПОСЛЕ настройки кодировки консоли: иначе em-dash в сообщении уедет в вопросы.
# Пишем прямо в stderr, а не Write-Warning: в PS 5.1 предупреждение уходит в stdout, получает
# локализованный префикс и переносится по 80 символов — подстрока в тесте перестаёт находиться.
if ($formatRank -eq 0) {
[Console]::Error.WriteLine("Malformed -FormatVersion '$FormatVersion' (expected N.N, e.g. 2.17)")
exit 1
}
if ($formatRank -lt (Get-FormatRank $formatVerifiedMin) -or $formatRank -gt (Get-FormatRank $formatVerifiedMax)) {
[Console]::Error.WriteLine("WARNING: Format version '$FormatVersion' is outside the tested range $formatVerifiedMin-$formatVerifiedMax — the scaffold is emitted as requested but was not verified on that platform")
}
$uuid1 = [guid]::NewGuid().ToString() $uuid1 = [guid]::NewGuid().ToString()
$uuid2 = [guid]::NewGuid().ToString() $uuid2 = [guid]::NewGuid().ToString()
$uuid3 = [guid]::NewGuid().ToString() $uuid3 = [guid]::NewGuid().ToString()
@@ -34,7 +58,7 @@ $uuid4 = [guid]::NewGuid().ToString()
$xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' $xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед style): # 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед style):
# платформа держит объявления по алфавиту, дописать в конец нельзя. # платформа держит объявления по алфавиту, дописать в конец нельзя.
if (($FormatVersion -match '^(\d+)\.(\d+)$') -and ([int]$Matches[1] * 100 + [int]$Matches[2]) -ge 221) { if ($formatRank -ge 221) {
$xmlnsDecl = $xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=' $xmlnsDecl = $xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
} }
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# epf-init v1.7 — Init 1C external data processor scaffold (+write_xml_file/write_utf8_bom: общий эталон записи) # epf-init v1.8 — Init 1C external data processor scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C external data processor.""" """Generates minimal XML source files for a 1C external data processor."""
import sys, os, re, argparse, uuid import sys, os, re, argparse, uuid
@@ -56,6 +56,10 @@ def format_rank(ver):
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0 return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
FORMAT_VERIFIED_MIN = "2.17"
FORMAT_VERIFIED_MAX = "2.21"
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -66,10 +70,22 @@ def main():
# Версия формата выгрузки. Своей конфигурации у автономного объекта нет, наследовать # Версия формата выгрузки. Своей конфигурации у автономного объекта нет, наследовать
# версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри объекта # версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри объекта
# навыки берут уже отсюда: их детектор читает version из корня этого файла. # навыки берут уже отсюда: их детектор читает version из корня этого файла.
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17', parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17')
choices=['2.17', '2.18', '2.19', '2.20', '2.21'])
args = ci_parse_args(parser) args = ci_parse_args(parser)
# Проверенный диапазон: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
# docs/1c-configuration-spec.md, «Лестница версий». Версии ниже 2.17 (платформы 8.3.23 и
# старше) реальны, поэтому запретом их не закрываем: за пределами диапазона —
# ПРЕДУПРЕЖДЕНИЕ, скаффолд всё равно выпускается. Ошибка — только на нечисловое значение.
format_rank_value = format_rank(args.FormatVersion)
if format_rank_value == 0:
print(f"Malformed -FormatVersion '{args.FormatVersion}' (expected N.N, e.g. 2.17)", file=sys.stderr)
sys.exit(1)
if not (format_rank(FORMAT_VERIFIED_MIN) <= format_rank_value <= format_rank(FORMAT_VERIFIED_MAX)):
print(f"WARNING: Format version '{args.FormatVersion}' is outside the tested range "
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — the scaffold is emitted as requested "
f"but was not verified on that platform", file=sys.stderr)
name = args.Name name = args.Name
synonym = args.Synonym if args.Synonym else name synonym = args.Synonym if args.Synonym else name
src_dir = args.SrcDir src_dir = args.SrcDir
@@ -24,7 +24,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка" python ".codex/skills/epf-validate/scripts/epf-validate.py" -ObjectPath "src/МояОбработка"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка/МояОбработка.xml" python ".codex/skills/epf-validate/scripts/epf-validate.py" -ObjectPath "src/МояОбработка/МояОбработка.xml"
``` ```
@@ -1,4 +1,4 @@
# epf-validate v1.4 — Validate 1C external data processor / report structure # epf-validate v1.5 — Validate 1C external data processor / report structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects # Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
param( param(
@@ -111,6 +111,19 @@ $finalize = {
} }
} }
# --- Format version ---
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
$formatVerifiedMin = "2.17"
$formatVerifiedMax = "2.21"
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
# --- Reference tables --- # --- Reference tables ---
$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' $guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
@@ -183,11 +196,15 @@ if ($root.NamespaceURI -ne $expectedNs) {
} }
$version = $root.GetAttribute("version") $version = $root.GetAttribute("version")
$versionRank = Get-FormatRank $version
if (-not $version) { if (-not $version) {
Report-Warn "1. Missing version attribute on MetaDataObject" Report-Warn "1. Missing version attribute on MetaDataObject"
} elseif ($version -notin @("2.17", "2.18", "2.19", "2.20", "2.21")) { } elseif ($versionRank -eq 0) {
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27). Report-Error "1. Malformed version '$version' (expected N.N)"
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20 or 2.21)" } elseif ($versionRank -lt (Get-FormatRank $formatVerifiedMin)) {
Report-Warn "1. Format version '$version' is below the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
} elseif ($versionRank -gt (Get-FormatRank $formatVerifiedMax)) {
Report-Warn "1. Format version '$version' is above the tested range $formatVerifiedMin-$formatVerifiedMax — skills were not verified on it"
} }
# Detect type: ExternalDataProcessor or ExternalReport # Detect type: ExternalDataProcessor or ExternalReport
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# epf-validate v1.4 — Validate 1C external data processor / report structure # epf-validate v1.5 — Validate 1C external data processor / report structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects # Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
@@ -60,6 +60,21 @@ CHILD_TYPE_ORDER = {
} }
# ── Format version ───────────────────────────────────────────
# Проверенный диапазон версий формата выгрузки: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
# docs/1c-configuration-spec.md, «Лестница версий». Версию задаёт платформа ВЫГРУЗКИ, а не режим
# совместимости конфигурации. Версии ниже 2.17 (платформы 8.3.23 и старше) существуют, но навыки
# на них не проверялись — это предупреждение о непокрытии, а не о некорректности файла.
FORMAT_VERIFIED_MIN = "2.17"
FORMAT_VERIFIED_MAX = "2.21"
def format_rank(ver):
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
def localname(el): def localname(el):
return etree.QName(el.tag).localname return etree.QName(el.tag).localname
@@ -185,11 +200,17 @@ def main():
check1_ok = False check1_ok = False
version = root.get("version", "") version = root.get("version", "")
version_rank = format_rank(version)
if not version: if not version:
report_warn("1. Missing version attribute on MetaDataObject") report_warn("1. Missing version attribute on MetaDataObject")
elif version not in ("2.17", "2.18", "2.19", "2.20", "2.21"): elif version_rank == 0:
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27). report_error(f"1. Malformed version '{version}' (expected N.N)")
report_warn(f"1. Unusual version '{version}' (expected 2.17-2.20 or 2.21)") elif version_rank < format_rank(FORMAT_VERIFIED_MIN):
report_warn(f"1. Format version '{version}' is below the tested range "
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
elif version_rank > format_rank(FORMAT_VERIFIED_MAX):
report_warn(f"1. Format version '{version}' is above the tested range "
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — skills were not verified on it")
# Detect type # Detect type
child_elements = [] child_elements = []
@@ -42,7 +42,7 @@ allowed-tools:
Используй общий скрипт из epf-build: Используй общий скрипт из epf-build:
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" <параметры> python ".codex/skills/epf-build/scripts/epf-build.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -66,8 +66,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-bu
```powershell ```powershell
# Сборка отчёта (файловая база) # Сборка отчёта (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf" python ".codex/skills/epf-build/scripts/epf-build.py" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf" python ".codex/skills/epf-build/scripts/epf-build.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
``` ```
@@ -41,7 +41,7 @@ allowed-tools:
Используй общий скрипт из epf-dump: Используй общий скрипт из epf-dump:
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" <параметры> python ".codex/skills/epf-dump/scripts/epf-dump.py" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
@@ -66,8 +66,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dum
```powershell ```powershell
# Разборка отчёта (файловая база) # Разборка отчёта (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src" python ".codex/skills/epf-dump/scripts/epf-dump.py" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
# Серверная база # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src" python ".codex/skills/epf-dump/scripts/epf-dump.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
``` ```
@@ -26,13 +26,19 @@ allowed-tools:
| Name | да | — | Имя отчёта (латиница/кириллица) | | Name | да | — | Имя отчёта (латиница/кириллица) |
| Synonym | нет | = Name | Синоним (отображаемое имя) | | Synonym | нет | = Name | Синоним (отображаемое имя) |
| SrcDir | нет | `src` | Каталог исходников относительно CWD | | SrcDir | нет | `src` | Каталог исходников относительно CWD |
| FormatVersion | нет | `2.17` | Версия формата: 2.20 — платформа 8.3.27, 2.21 — 8.5. Дефолт открывается любой платформой | | FormatVersion | нет | `2.17` | Версия формата выгрузки — см. ниже |
| --WithSKD | нет | — | Создать пустую СКД и привязать к MainDataCompositionSchema | | --WithSKD | нет | — | Создать пустую СКД и привязать к MainDataCompositionSchema |
`FormatVersion`**не выше** версии формата платформы, на которой объект будут собирать и открывать:
8.3.24 — `2.17`, 8.3.25 — `2.18`, 8.3.26 — `2.19`, 8.3.27 — `2.20`, 8.5 — `2.21`. Ниже брать можно:
платформа читает свой формат и любой более старый, поэтому дефолт `2.17` подходит для всей линейки
8.3.24 и выше. Для более старых платформ счёт идёт так же, по одной версии на релиз (8.3.23 — `2.16`),
но на них навыки не проверялись — такое значение принимается с предупреждением.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-FormatVersion "<2.17|2.18|2.19|2.20|2.21>"] [-WithSKD] python ".codex/skills/erf-init/scripts/init.py" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-FormatVersion "<версия>"] [-WithSKD]
``` ```
## Дальнейшие шаги ## Дальнейшие шаги
@@ -1,4 +1,4 @@
# erf-init v1.7 — Init 1C external report scaffold (+write_xml_file/write_utf8_bom: общий эталон записи) # erf-init v1.8 — Init 1C external report scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -13,12 +13,25 @@ param(
# Версия формата выгрузки. Своей конфигурации у автономного отчёта нет, наследовать # Версия формата выгрузки. Своей конфигурации у автономного отчёта нет, наследовать
# версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри отчёта # версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри отчёта
# навыки берут уже отсюда: их детектор читает version из корня этого файла. # навыки берут уже отсюда: их детектор читает version из корня этого файла.
[ValidateSet("2.17", "2.18", "2.19", "2.20", "2.21")]
[string]$FormatVersion = "2.17" [string]$FormatVersion = "2.17"
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
# --- Format version ---
# Проверенный диапазон: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница — docs/1c-configuration-spec.md,
# «Лестница версий». Версии ниже 2.17 (платформы 8.3.23 и старше) реальны, поэтому запретом их не
# закрываем: за пределами диапазона — ПРЕДУПРЕЖДЕНИЕ, скаффолд всё равно выпускается. Ошибка — только
# на нечисловое значение: это опечатка, а не версия.
$formatVerifiedMin = "2.17"
$formatVerifiedMax = "2.21"
# Версия формата как число: "2.20" → 220. Строковое сравнение неверно ("2.9" > "2.17").
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
$formatRank = Get-FormatRank $FormatVersion
function Esc-XmlText { function Esc-XmlText {
param([string]$s) param([string]$s)
# Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми. # Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми.
@@ -27,6 +40,17 @@ function Esc-XmlText {
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8 [Console]::InputEncoding = [System.Text.Encoding]::UTF8
# Проверка версии — ПОСЛЕ настройки кодировки консоли: иначе em-dash в сообщении уедет в вопросы.
# Пишем прямо в stderr, а не Write-Warning: в PS 5.1 предупреждение уходит в stdout, получает
# локализованный префикс и переносится по 80 символов — подстрока в тесте перестаёт находиться.
if ($formatRank -eq 0) {
[Console]::Error.WriteLine("Malformed -FormatVersion '$FormatVersion' (expected N.N, e.g. 2.17)")
exit 1
}
if ($formatRank -lt (Get-FormatRank $formatVerifiedMin) -or $formatRank -gt (Get-FormatRank $formatVerifiedMax)) {
[Console]::Error.WriteLine("WARNING: Format version '$FormatVersion' is outside the tested range $formatVerifiedMin-$formatVerifiedMax — the scaffold is emitted as requested but was not verified on that platform")
}
$uuid1 = [guid]::NewGuid().ToString() $uuid1 = [guid]::NewGuid().ToString()
$uuid2 = [guid]::NewGuid().ToString() $uuid2 = [guid]::NewGuid().ToString()
$uuid3 = [guid]::NewGuid().ToString() $uuid3 = [guid]::NewGuid().ToString()
@@ -36,7 +60,7 @@ $uuid4 = [guid]::NewGuid().ToString()
$xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' $xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед style): # 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед style):
# платформа держит объявления по алфавиту, дописать в конец нельзя. # платформа держит объявления по алфавиту, дописать в конец нельзя.
if (($FormatVersion -match '^(\d+)\.(\d+)$') -and ([int]$Matches[1] * 100 + [int]$Matches[2]) -ge 221) { if ($formatRank -ge 221) {
$xmlnsDecl = $xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=' $xmlnsDecl = $xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
} }
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# erf-init v1.7 — Init 1C external report scaffold (+write_xml_file/write_utf8_bom: общий эталон записи) # erf-init v1.8 — Init 1C external report scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C external report.""" """Generates minimal XML source files for a 1C external report."""
import sys, os, re, argparse, uuid import sys, os, re, argparse, uuid
@@ -56,6 +56,10 @@ def format_rank(ver):
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0 return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
FORMAT_VERIFIED_MIN = "2.17"
FORMAT_VERIFIED_MAX = "2.21"
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -66,11 +70,23 @@ def main():
# Версия формата выгрузки. Своей конфигурации у автономного объекта нет, наследовать # Версия формата выгрузки. Своей конфигурации у автономного объекта нет, наследовать
# версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри объекта # версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри объекта
# навыки берут уже отсюда: их детектор читает version из корня этого файла. # навыки берут уже отсюда: их детектор читает version из корня этого файла.
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17', parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17')
choices=['2.17', '2.18', '2.19', '2.20', '2.21'])
parser.add_argument('-WithSKD', dest='WithSKD', action='store_true') parser.add_argument('-WithSKD', dest='WithSKD', action='store_true')
args = ci_parse_args(parser) args = ci_parse_args(parser)
# Проверенный диапазон: 2.17 (8.3.24) … 2.21 (8.5). Полная лестница —
# docs/1c-configuration-spec.md, «Лестница версий». Версии ниже 2.17 (платформы 8.3.23 и
# старше) реальны, поэтому запретом их не закрываем: за пределами диапазона —
# ПРЕДУПРЕЖДЕНИЕ, скаффолд всё равно выпускается. Ошибка — только на нечисловое значение.
format_rank_value = format_rank(args.FormatVersion)
if format_rank_value == 0:
print(f"Malformed -FormatVersion '{args.FormatVersion}' (expected N.N, e.g. 2.17)", file=sys.stderr)
sys.exit(1)
if not (format_rank(FORMAT_VERIFIED_MIN) <= format_rank_value <= format_rank(FORMAT_VERIFIED_MAX)):
print(f"WARNING: Format version '{args.FormatVersion}' is outside the tested range "
f"{FORMAT_VERIFIED_MIN}-{FORMAT_VERIFIED_MAX} — the scaffold is emitted as requested "
f"but was not verified on that platform", file=sys.stderr)
name = args.Name name = args.Name
synonym = args.Synonym if args.Synonym else name synonym = args.Synonym if args.Synonym else name
src_dir = args.SrcDir src_dir = args.SrcDir
@@ -26,7 +26,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт" python ".codex/skills/epf-validate/scripts/epf-validate.py" -ObjectPath "src/МойОтчёт"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт/МойОтчёт.xml" python ".codex/skills/epf-validate/scripts/epf-validate.py" -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
``` ```
@@ -32,7 +32,7 @@ allowed-tools:
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-add.ps1" -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault] python ".codex/skills/form-add/scripts/form-add.py" -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault]
``` ```
## Purpose — назначение формы ## Purpose — назначение формы
@@ -29,10 +29,10 @@ allowed-tools:
```powershell ```powershell
# Режим JSON DSL # Режим JSON DSL
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -JsonPath "<json>" -OutputPath "<Form.xml>" python ".codex/skills/form-compile/scripts/form-compile.py" -JsonPath "<json>" -OutputPath "<Form.xml>"
# Режим from-object (объект и purpose выводятся из OutputPath; Document и Catalog) # Режим from-object (объект и purpose выводятся из OutputPath; Document и Catalog)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -FromObject -OutputPath "<.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml>" python ".codex/skills/form-compile/scripts/form-compile.py" -FromObject -OutputPath "<.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml>"
``` ```
## JSON DSL — справка ## JSON DSL — справка

Some files were not shown because too many files have changed in this diff Show More