mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-16 00:09:01 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fae79a075f |
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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 недоступен."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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/"
|
|
||||||
}
|
|
||||||
@@ -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 — валидировать
|
|
||||||
```
|
|
||||||
@@ -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('&','&').Replace('<','<').Replace('>','>').Replace('"','"')
|
|
||||||
}
|
|
||||||
|
|
||||||
function Esc-XmlText {
|
|
||||||
# Экранирование ТЕКСТА элемента: только & < > . Кавычки в тексте платформа НЕ экранирует —
|
|
||||||
# пишет литерально (проверено: 92142 сырых кавычки на корпус, ни одной "). " платформа
|
|
||||||
# принимает, но при выгрузке нормализует обратно в кавычку → лишний шум в роундтрипе.
|
|
||||||
param([string]$s)
|
|
||||||
return $s.Replace('&','&').Replace('<','<').Replace('>','>')
|
|
||||||
}
|
|
||||||
|
|
||||||
# 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('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
|
||||||
|
|
||||||
|
|
||||||
def esc_xml_text(s):
|
|
||||||
"""Экранирование ТЕКСТА элемента: только & < > . Кавычки платформа в тексте не экранирует
|
|
||||||
(92142 сырых кавычки на корпус, ни одной "); " она принимает, но нормализует обратно."""
|
|
||||||
return s.replace('&', '&').replace('<', '<').replace('>', '>')
|
|
||||||
|
|
||||||
|
|
||||||
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&Модификатор"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
- `&` в условии автоматически экранируется в `&` в 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 "<путь>"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Три режима
|
## Три режима
|
||||||
@@ -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 — валидировать
|
||||||
|
```
|
||||||
+46
-8
@@ -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) {
|
||||||
+45
-11
@@ -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"
|
||||||
```
|
```
|
||||||
+21
-4
@@ -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
|
||||||
+24
-4
@@ -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>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Конфигурацию-источник передавай и валидатору: заимствованные формы он проверяет по ней.
|
||||||
|
|
||||||
+437
-80
@@ -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)
|
||||||
+445
-81
@@ -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"
|
||||||
```
|
```
|
||||||
+245
-5
@@ -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
|
||||||
+240
-4
@@ -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"
|
||||||
```
|
```
|
||||||
+53
-1
@@ -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 {
|
||||||
+53
-1
@@ -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
|
||||||
```
|
```
|
||||||
+41
-26
@@ -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 }
|
||||||
}
|
}
|
||||||
+43
-22
@@ -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 "МоёРасширение"
|
||||||
```
|
```
|
||||||
+53
-1
@@ -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 {
|
||||||
+53
-1
@@ -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"
|
||||||
```
|
```
|
||||||
+42
-4
@@ -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
|
||||||
+70
-7
@@ -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"
|
||||||
```
|
```
|
||||||
|
|
||||||
+21
-4
@@ -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
|
||||||
+25
-4
@@ -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
Reference in New Issue
Block a user