mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-21 10:39:42 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb1169c245 |
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "cc-1c-skills",
|
|
||||||
"interface": {
|
|
||||||
"displayName": "1C Skills"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "1c-skills",
|
|
||||||
"source": {
|
|
||||||
"source": "url",
|
|
||||||
"url": "https://github.com/Nikolay-Shirokov/cc-1c-skills.git",
|
|
||||||
"ref": "port-codex"
|
|
||||||
},
|
|
||||||
"policy": {
|
|
||||||
"installation": "AVAILABLE"
|
|
||||||
},
|
|
||||||
"category": "Development"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "1c-skills-py",
|
|
||||||
"source": {
|
|
||||||
"source": "url",
|
|
||||||
"url": "https://github.com/Nikolay-Shirokov/cc-1c-skills.git",
|
|
||||||
"ref": "port-codex-py"
|
|
||||||
},
|
|
||||||
"policy": {
|
|
||||||
"installation": "AVAILABLE"
|
|
||||||
},
|
|
||||||
"category": "Development"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -24,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'
|
powershell.exe -NoProfile -File ".augment/skills/cf-edit/scripts/cf-edit.ps1" -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 "<путь>"
|
powershell.exe -NoProfile -File ".augment/skills/cf-info/scripts/cf-info.ps1" -ConfigPath "<путь>"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Три режима
|
## Три режима
|
||||||
+3
-2
@@ -1,7 +1,8 @@
|
|||||||
# cf-info v1.5 — Compact summary of 1C configuration root
|
# cf-info v1.6 — Compact summary of 1C configuration root
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
|
[Parameter(Mandatory=$true, Position=0)][Alias('Path')][string]$ConfigPath,
|
||||||
[ValidateSet("overview","brief","full")]
|
[ValidateSet("overview","brief","full")]
|
||||||
[string]$Mode = "overview",
|
[string]$Mode = "overview",
|
||||||
[Alias('Name')]
|
[Alias('Name')]
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-info v1.5 — Compact summary of 1C configuration root
|
# cf-info v1.6 — Compact summary of 1C configuration root
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -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
|
||||||
|
powershell.exe -NoProfile -File ".augment/skills/cf-init/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
|
||||||
|
|
||||||
|
# Под платформу 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"
|
powershell.exe -NoProfile -File ".augment/skills/cf-validate/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty"
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty/Configuration.xml"
|
powershell.exe -NoProfile -File ".augment/skills/cf-validate/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty/Configuration.xml"
|
||||||
```
|
```
|
||||||
+23
-5
@@ -1,7 +1,8 @@
|
|||||||
# cf-validate v1.6 — Validate 1C configuration root structure
|
# cf-validate v1.8 — Validate 1C configuration root structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory, Position=0)]
|
||||||
[Alias('Path')]
|
[Alias('Path')]
|
||||||
[string]$ConfigPath,
|
[string]$ConfigPath,
|
||||||
|
|
||||||
@@ -89,6 +90,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 +217,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.8 — 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
|
||||||
@@ -31,6 +31,7 @@ allowed-tools:
|
|||||||
| `ExtensionPath` | Путь к каталогу расширения (обязат.) |
|
| `ExtensionPath` | Путь к каталогу расширения (обязат.) |
|
||||||
| `ConfigPath` | Путь к конфигурации-источнику (обязат.) |
|
| `ConfigPath` | Путь к конфигурации-источнику (обязат.) |
|
||||||
| `Object` | Что заимствовать (обязат.), batch через `;;` |
|
| `Object` | Что заимствовать (обязат.), batch через `;;` |
|
||||||
|
| `Module` | Создать пустые модули объекта: `ObjectModule`, `ManagerModule`, `RecordSetModule`, `ValueManagerModule` (через запятую) или `None`. У типов с единственным модулем (`CommonModule`, `HTTPService`, `WebService`) он создаётся и без параметра |
|
||||||
| `BorrowMainAttribute` | Заимствовать основной реквизит формы. Без параметра — не заимствует. `Form` — реквизиты, используемые на форме. `All` — все реквизиты объекта. Требует форму в -Object |
|
| `BorrowMainAttribute` | Заимствовать основной реквизит формы. Без параметра — не заимствует. `Form` — реквизиты, используемые на форме. `All` — все реквизиты объекта. Требует форму в -Object |
|
||||||
|
|
||||||
## Формат -Object
|
## Формат -Object
|
||||||
@@ -65,36 +66,44 @@ allowed-tools:
|
|||||||
2. `/meta-edit` — добавить новый реквизит в объект расширения
|
2. `/meta-edit` — добавить новый реквизит в объект расширения
|
||||||
3. `/form-edit` — вывести реквизит на заимствованную форму
|
3. `/form-edit` — вывести реквизит на заимствованную форму
|
||||||
|
|
||||||
**Защита существующих данных**: если зависимый объект уже заимствован с содержимым (реквизитами, формами) — скрипт не перезаписывает его, а добавляет только недостающее.
|
**Защита существующих данных**: уже заимствованный объект не перезаписывается — добавляется только недостающее. Повторный вызов безопасен: собственные реквизиты расширения, заимствованные подобъекты и код в модулях сохраняются.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
powershell.exe -NoProfile -File ".augment/skills/cfe-borrow/scripts/cfe-borrow.ps1" -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\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты" -Module ObjectModule,ManagerModule
|
||||||
|
|
||||||
|
# Общий модуль без файла модуля
|
||||||
|
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "CommonModule.РаботаСФайлами" -Module None
|
||||||
|
|
||||||
# Заимствовать форму (автоматически заимствует родительский объект)
|
# Заимствовать форму (автоматически заимствует родительский объект)
|
||||||
... -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>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Конфигурацию-источник передавай и валидатору: заимствованные формы он проверяет по ней.
|
||||||
|
|
||||||
+612
-95
@@ -1,10 +1,11 @@
|
|||||||
# cfe-borrow v1.20 — Borrow objects from configuration into extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
|
# cfe-borrow v1.32 — 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,
|
||||||
[Parameter(Mandatory)][string]$ConfigPath,
|
[Parameter(Mandatory)][string]$ConfigPath,
|
||||||
[Parameter(Mandatory)][string]$Object,
|
[Parameter(Mandatory)][string]$Object,
|
||||||
[string]$BorrowMainAttribute
|
[string]$BorrowMainAttribute,
|
||||||
|
[string]$Module
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
@@ -16,18 +17,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 +59,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
|
||||||
@@ -128,6 +262,32 @@ $childTypeDirMap = @{
|
|||||||
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "Bot"="Bots"; "Language"="Languages"
|
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "Bot"="Bots"; "Language"="Languages"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- 4a. Модули заимствованных объектов ---
|
||||||
|
# Порядок внутри значения — порядок выгрузки Конфигуратора: сначала «объектный» модуль
|
||||||
|
# (ObjectModule / RecordSetModule / ValueManagerModule), затем ManagerModule.
|
||||||
|
$script:moduleKindsByType = @{
|
||||||
|
"CommonModule"=@("Module"); "HTTPService"=@("Module"); "WebService"=@("Module")
|
||||||
|
"Catalog"=@("ObjectModule","ManagerModule"); "Document"=@("ObjectModule","ManagerModule")
|
||||||
|
"Report"=@("ObjectModule","ManagerModule"); "DataProcessor"=@("ObjectModule","ManagerModule")
|
||||||
|
"ExchangePlan"=@("ObjectModule","ManagerModule")
|
||||||
|
"ChartOfCharacteristicTypes"=@("ObjectModule","ManagerModule")
|
||||||
|
"ChartOfAccounts"=@("ObjectModule","ManagerModule")
|
||||||
|
"ChartOfCalculationTypes"=@("ObjectModule","ManagerModule")
|
||||||
|
"BusinessProcess"=@("ObjectModule","ManagerModule"); "Task"=@("ObjectModule","ManagerModule")
|
||||||
|
"InformationRegister"=@("RecordSetModule","ManagerModule")
|
||||||
|
"AccumulationRegister"=@("RecordSetModule","ManagerModule")
|
||||||
|
"AccountingRegister"=@("RecordSetModule","ManagerModule")
|
||||||
|
"CalculationRegister"=@("RecordSetModule","ManagerModule")
|
||||||
|
"Sequence"=@("RecordSetModule","ManagerModule")
|
||||||
|
"Constant"=@("ValueManagerModule","ManagerModule")
|
||||||
|
"Enum"=@("ManagerModule"); "DocumentJournal"=@("ManagerModule")
|
||||||
|
"FilterCriterion"=@("ManagerModule")
|
||||||
|
}
|
||||||
|
# Типы с ЕДИНСТВЕННЫМ модулем: ради него объект и заимствуют, поэтому файл создаётся молча.
|
||||||
|
# Отказ — `-Module None`.
|
||||||
|
$script:autoModuleTypes = @("CommonModule", "HTTPService", "WebService")
|
||||||
|
$script:moduleKindNames = @("Module", "ObjectModule", "ManagerModule", "RecordSetModule", "ValueManagerModule")
|
||||||
|
|
||||||
# --- 4b. Russian synonym → English type ---
|
# --- 4b. Russian synonym → English type ---
|
||||||
$synonymMap = @{
|
$synonymMap = @{
|
||||||
"Справочник"="Catalog"; "Документ"="Document"; "Перечисление"="Enum"
|
"Справочник"="Catalog"; "Документ"="Document"; "Перечисление"="Enum"
|
||||||
@@ -209,7 +369,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 +384,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 +392,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 +410,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 +422,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 +489,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")
|
||||||
|
|
||||||
@@ -439,7 +618,82 @@ if ($BorrowMainAttribute) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- 9c. Validate -Module ---
|
||||||
|
$script:requestedModules = @()
|
||||||
|
$script:noModule = $false
|
||||||
|
if ($Module) {
|
||||||
|
foreach ($raw in ($Module -split '[,;]')) {
|
||||||
|
$kind = $raw.Trim()
|
||||||
|
if (-not $kind) { continue }
|
||||||
|
# Сравнение РЕГИСТРОНЕЗАВИСИМОЕ явно (-ieq): в py-порте это отдельная ветка, и молчаливое
|
||||||
|
# расхождение портов на «none» ловится только глазами.
|
||||||
|
if ($kind -ieq "None") { $script:noModule = $true; continue }
|
||||||
|
$canon = @($script:moduleKindNames | Where-Object { $_ -ieq $kind })
|
||||||
|
if ($canon.Count -eq 0) {
|
||||||
|
Write-Error "Неизвестный вид модуля '$kind'. Допустимо: $($script:moduleKindNames -join ', '), None"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$script:requestedModules += $canon[0]
|
||||||
|
}
|
||||||
|
if ($script:noModule -and $script:requestedModules.Count -gt 0) {
|
||||||
|
Write-Error "-Module None нельзя сочетать с видами модулей"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Какие модули создать для объекта. Тип с единственным модулем получает его всегда — уточнять
|
||||||
|
# там нечего; -Module разбирает только неоднозначные типы. Иначе батч смешанных типов
|
||||||
|
# (`CommonModule.X ;; Catalog.Y`) не выражался бы одним вызовом.
|
||||||
|
function Resolve-ModuleKinds {
|
||||||
|
param([string]$typeName)
|
||||||
|
|
||||||
|
if ($script:noModule) { return @() }
|
||||||
|
$allowed = @($script:moduleKindsByType[$typeName])
|
||||||
|
if ($allowed.Count -eq 0) { return @() }
|
||||||
|
|
||||||
|
if ($script:autoModuleTypes -contains $typeName) { return @($allowed[0]) }
|
||||||
|
if ($script:requestedModules.Count -eq 0) { return @() }
|
||||||
|
|
||||||
|
# Порядок берём из таблицы типа, а не из порядка ключей в -Module.
|
||||||
|
$selected = @($allowed | Where-Object { $script:requestedModules -contains $_ })
|
||||||
|
if ($selected.Count -eq 0) {
|
||||||
|
Warn " Тип $typeName не имеет запрошенных модулей — пропущено. Допустимо: $($allowed -join ', ')"
|
||||||
|
}
|
||||||
|
return $selected
|
||||||
|
}
|
||||||
|
|
||||||
# --- 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 +753,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 +886,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 +943,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 +961,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 +1182,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 +1218,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 +1252,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).
|
||||||
@@ -1087,6 +1384,81 @@ function Test-ObjectBorrowed {
|
|||||||
return (Test-Path $objFile)
|
return (Test-Path $objFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- 10f. Helper: пометка расширенного свойства (<xr:PropertyState>) ---
|
||||||
|
# Свойство появилось в формате 2.19 (8.3.26): на 2.18 и ниже платформа молча выбрасывает элемент
|
||||||
|
# при загрузке. С 2.19 Конфигуратор ставит его сам при выгрузке — эмитим, чтобы исходники навыка
|
||||||
|
# совпадали с эталоном. Имя свойства = базовое имя файла модуля (Module / ObjectModule / …),
|
||||||
|
# у заимствованной формы — Form. Ставит тот, кто создал файл модуля (или форму).
|
||||||
|
function Build-PropertyStateXml {
|
||||||
|
param([string]$propertyName, [string]$indent)
|
||||||
|
|
||||||
|
$sb = New-Object System.Text.StringBuilder
|
||||||
|
$sb.AppendLine("${indent}<xr:PropertyState>") | Out-Null
|
||||||
|
$sb.AppendLine("${indent}`t<xr:Property>${propertyName}</xr:Property>") | Out-Null
|
||||||
|
$sb.AppendLine("${indent}`t<xr:State>Extended</xr:State>") | Out-Null
|
||||||
|
$sb.Append("${indent}</xr:PropertyState>") | Out-Null
|
||||||
|
return $sb.ToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-PropertyStateFlag {
|
||||||
|
param([string]$objFile, [string]$propertyName, [string]$formatVersion)
|
||||||
|
|
||||||
|
if ((Get-FormatRank $formatVersion) -lt 219) { return }
|
||||||
|
if (-not (Test-Path $objFile)) { return }
|
||||||
|
|
||||||
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
$text = [System.IO.File]::ReadAllText($objFile, $enc)
|
||||||
|
$nl = if ($text -match "`r`n") { "`r`n" } else { "`n" }
|
||||||
|
|
||||||
|
# ПЕРВЫЙ <InternalInfo> в файле — собственный у объекта: у реквизитов и подобъектов свои,
|
||||||
|
# но они лежат ниже, внутри <ChildObjects>.
|
||||||
|
$empty = [regex]::Match($text, '([ \t]*)<InternalInfo\s*/>')
|
||||||
|
$open = [regex]::Match($text, '(?s)([ \t]*)<InternalInfo>(.*?)</InternalInfo>')
|
||||||
|
|
||||||
|
if ($empty.Success -and (-not $open.Success -or $empty.Index -lt $open.Index)) {
|
||||||
|
$ind = $empty.Groups[1].Value
|
||||||
|
$block = Build-PropertyStateXml $propertyName ($ind + "`t")
|
||||||
|
$replacement = "${ind}<InternalInfo>${nl}${block}${nl}${ind}</InternalInfo>"
|
||||||
|
$text = $text.Remove($empty.Index, $empty.Length).Insert($empty.Index, $replacement)
|
||||||
|
} elseif ($open.Success) {
|
||||||
|
if ($open.Groups[2].Value -match "<xr:Property>$([regex]::Escape($propertyName))</xr:Property>") { return }
|
||||||
|
$ind = $open.Groups[1].Value
|
||||||
|
$block = Build-PropertyStateXml $propertyName ($ind + "`t")
|
||||||
|
# Дописываем в КОНЕЦ InternalInfo: у Конфигуратора PropertyState идёт после GeneratedType.
|
||||||
|
$closeAt = $open.Index + $open.Length - "</InternalInfo>".Length - $ind.Length
|
||||||
|
$text = $text.Insert($closeAt, "${block}${nl}")
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
[System.IO.File]::WriteAllText($objFile, $text, $enc)
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 10g. Helper: пустой модуль заимствованного объекта ---
|
||||||
|
function New-BorrowedModuleFile {
|
||||||
|
param([string]$typeName, [string]$objName, [string]$moduleKind)
|
||||||
|
|
||||||
|
$dirName = $childTypeDirMap[$typeName]
|
||||||
|
$objDir = Join-Path (Join-Path $extDir $dirName) $objName
|
||||||
|
$moduleDir = Join-Path $objDir "Ext"
|
||||||
|
if (-not (Test-Path $moduleDir)) { New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null }
|
||||||
|
|
||||||
|
# NEVER overwrite an existing one: повторное заимствование не должно затирать дописанный код
|
||||||
|
# (то же правило, что у модуля формы).
|
||||||
|
$moduleFile = Join-Path $moduleDir "${moduleKind}.bsl"
|
||||||
|
if (Test-Path $moduleFile) {
|
||||||
|
Info " Preserved existing ${moduleKind}.bsl"
|
||||||
|
} else {
|
||||||
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
[System.IO.File]::WriteAllText($moduleFile, "", $enc)
|
||||||
|
Info " Created: $moduleFile"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Флаг ставим и для уже существовавшего файла: состояние объекта должно отражать факт модуля.
|
||||||
|
Set-PropertyStateFlag (Join-Path (Join-Path $extDir $dirName) "${objName}.xml") $moduleKind $script:formatVersion
|
||||||
|
return $moduleFile
|
||||||
|
}
|
||||||
|
|
||||||
# --- 11. Helper: generate InternalInfo XML ---
|
# --- 11. Helper: generate InternalInfo XML ---
|
||||||
function Build-InternalInfoXml {
|
function Build-InternalInfoXml {
|
||||||
param([string]$typeName, [string]$objName, [string]$indent)
|
param([string]$typeName, [string]$objName, [string]$indent)
|
||||||
@@ -1120,8 +1492,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 +1542,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 +1560,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 +1574,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 +1648,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 +1664,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 +1714,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 +1735,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 +1748,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 +1871,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 +1879,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 +1898,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 +1934,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 +1969,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 +2002,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 +2017,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 +2205,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)
|
||||||
@@ -1888,6 +2350,9 @@ foreach ($item in $items) {
|
|||||||
$hasBMA = [bool]$BorrowMainAttribute
|
$hasBMA = [bool]$BorrowMainAttribute
|
||||||
$formFiles = Borrow-Form $typeName $objName $formName -BorrowMainAttr:$hasBMA
|
$formFiles = Borrow-Form $typeName $objName $formName -BorrowMainAttr:$hasBMA
|
||||||
$script:borrowedFiles += $formFiles
|
$script:borrowedFiles += $formFiles
|
||||||
|
# Замер на 8.3.26: платформа помечает форму расширенной сразу при заимствовании,
|
||||||
|
# даже если элементы не менялись. Флаг живёт в метаданных формы, не у владельца.
|
||||||
|
Set-PropertyStateFlag $formFiles[0] "Form" $script:formatVersion
|
||||||
$borrowedCount++
|
$borrowedCount++
|
||||||
|
|
||||||
# Borrow main attribute if requested
|
# Borrow main attribute if requested
|
||||||
@@ -1896,30 +2361,82 @@ foreach ($item in $items) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
# --- Object borrowing (existing logic) ---
|
# --- Object borrowing (existing logic) ---
|
||||||
Info "Borrowing ${typeName}.${objName}..."
|
|
||||||
|
|
||||||
$src = Read-SourceObject $typeName $objName
|
|
||||||
Info " Source UUID: $($src.Uuid)"
|
|
||||||
|
|
||||||
$borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties
|
|
||||||
|
|
||||||
$targetDir = Join-Path $extDir $dirName
|
$targetDir = Join-Path $extDir $dirName
|
||||||
if (-not (Test-Path $targetDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
$targetFile = Join-Path $targetDir "${objName}.xml"
|
$targetFile = Join-Path $targetDir "${objName}.xml"
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
[System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc)
|
# Уже заимствованный объект НЕ переписываем: в его XML лежат собственные реквизиты
|
||||||
Info " Created: $targetFile"
|
# расширения, заимствованные подобъекты и состояния, которые из источника не выводятся.
|
||||||
|
# Повторный вызов — законный способ доделать модуль (-Module), а не переиздать заготовку.
|
||||||
|
if (Test-ObjectBorrowed $typeName $objName) {
|
||||||
|
Info "Already borrowed: ${typeName}.${objName} — XML сохранён без изменений"
|
||||||
|
} else {
|
||||||
|
Info "Borrowing ${typeName}.${objName}..."
|
||||||
|
|
||||||
|
$src = Read-SourceObject $typeName $objName
|
||||||
|
Info " Source UUID: $($src.Uuid)"
|
||||||
|
|
||||||
|
$borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties
|
||||||
|
|
||||||
|
if (-not (Test-Path $targetDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
[System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc)
|
||||||
|
Info " Created: $targetFile"
|
||||||
|
}
|
||||||
|
|
||||||
Add-ToChildObjects $typeName $objName
|
Add-ToChildObjects $typeName $objName
|
||||||
|
|
||||||
$script:borrowedFiles += $targetFile
|
$script:borrowedFiles += $targetFile
|
||||||
|
foreach ($kind in (Resolve-ModuleKinds $typeName)) {
|
||||||
|
$script:borrowedFiles += (New-BorrowedModuleFile $typeName $objName $kind)
|
||||||
|
}
|
||||||
$borrowedCount++
|
$borrowedCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- 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)
|
||||||
+604
-92
@@ -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.32 — 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
|
||||||
@@ -105,6 +240,32 @@ CHILD_TYPE_DIR_MAP = {
|
|||||||
"Bot": "Bots", "Language": "Languages",
|
"Bot": "Bots", "Language": "Languages",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Модули заимствованных объектов ---
|
||||||
|
# Порядок внутри значения — порядок выгрузки Конфигуратора: сначала «объектный» модуль
|
||||||
|
# (ObjectModule / RecordSetModule / ValueManagerModule), затем ManagerModule.
|
||||||
|
MODULE_KINDS_BY_TYPE = {
|
||||||
|
"CommonModule": ["Module"], "HTTPService": ["Module"], "WebService": ["Module"],
|
||||||
|
"Catalog": ["ObjectModule", "ManagerModule"], "Document": ["ObjectModule", "ManagerModule"],
|
||||||
|
"Report": ["ObjectModule", "ManagerModule"], "DataProcessor": ["ObjectModule", "ManagerModule"],
|
||||||
|
"ExchangePlan": ["ObjectModule", "ManagerModule"],
|
||||||
|
"ChartOfCharacteristicTypes": ["ObjectModule", "ManagerModule"],
|
||||||
|
"ChartOfAccounts": ["ObjectModule", "ManagerModule"],
|
||||||
|
"ChartOfCalculationTypes": ["ObjectModule", "ManagerModule"],
|
||||||
|
"BusinessProcess": ["ObjectModule", "ManagerModule"], "Task": ["ObjectModule", "ManagerModule"],
|
||||||
|
"InformationRegister": ["RecordSetModule", "ManagerModule"],
|
||||||
|
"AccumulationRegister": ["RecordSetModule", "ManagerModule"],
|
||||||
|
"AccountingRegister": ["RecordSetModule", "ManagerModule"],
|
||||||
|
"CalculationRegister": ["RecordSetModule", "ManagerModule"],
|
||||||
|
"Sequence": ["RecordSetModule", "ManagerModule"],
|
||||||
|
"Constant": ["ValueManagerModule", "ManagerModule"],
|
||||||
|
"Enum": ["ManagerModule"], "DocumentJournal": ["ManagerModule"],
|
||||||
|
"FilterCriterion": ["ManagerModule"],
|
||||||
|
}
|
||||||
|
# Типы с ЕДИНСТВЕННЫМ модулем: ради него объект и заимствуют, поэтому файл создаётся молча.
|
||||||
|
# Отказ — `-Module None`.
|
||||||
|
AUTO_MODULE_TYPES = ["CommonModule", "HTTPService", "WebService"]
|
||||||
|
MODULE_KIND_NAMES = ["Module", "ObjectModule", "ManagerModule", "RecordSetModule", "ValueManagerModule"]
|
||||||
|
|
||||||
SYNONYM_MAP = {
|
SYNONYM_MAP = {
|
||||||
"\u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a": "Catalog",
|
"\u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a": "Catalog",
|
||||||
"\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442": "Document",
|
"\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442": "Document",
|
||||||
@@ -202,6 +363,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 +377,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 +385,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 +403,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 +415,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 +481,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",
|
||||||
@@ -362,6 +542,55 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
# --- Пометка расширенного свойства (<xr:PropertyState>) ---
|
||||||
|
# Свойство появилось в формате 2.19 (8.3.26): на 2.18 и ниже платформа молча выбрасывает элемент
|
||||||
|
# при загрузке. С 2.19 Конфигуратор ставит его сам при выгрузке. Правило: флаг ставит тот, кто
|
||||||
|
# создал файл модуля, — здесь это мы. Имя свойства = базовое имя файла модуля.
|
||||||
|
# Копии этих функций есть в cfe-patch-method (навыки автономны); держать их одинаковыми — сознательно.
|
||||||
|
def build_property_state_xml(property_name, indent):
|
||||||
|
return "\n".join([
|
||||||
|
f"{indent}<xr:PropertyState>",
|
||||||
|
f"{indent}\t<xr:Property>{property_name}</xr:Property>",
|
||||||
|
f"{indent}\t<xr:State>Extended</xr:State>",
|
||||||
|
f"{indent}</xr:PropertyState>",
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def set_property_state_flag(obj_file, property_name, format_version):
|
||||||
|
if format_rank(format_version) < 219:
|
||||||
|
return
|
||||||
|
if not os.path.isfile(obj_file):
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(obj_file, "r", encoding="utf-8-sig", newline="") as fh:
|
||||||
|
text = fh.read()
|
||||||
|
nl = "\r\n" if "\r\n" in text else "\n"
|
||||||
|
|
||||||
|
# ПЕРВЫЙ <InternalInfo> в файле — собственный у объекта: у реквизитов и подобъектов свои,
|
||||||
|
# но они лежат ниже, внутри <ChildObjects>.
|
||||||
|
empty = re.search(r"([ \t]*)<InternalInfo\s*/>", text)
|
||||||
|
opened = re.search(r"([ \t]*)<InternalInfo>(.*?)</InternalInfo>", text, re.S)
|
||||||
|
|
||||||
|
if empty and (not opened or empty.start() < opened.start()):
|
||||||
|
ind = empty.group(1)
|
||||||
|
block = build_property_state_xml(property_name, ind + "\t")
|
||||||
|
replacement = f"{ind}<InternalInfo>{nl}{block}{nl}{ind}</InternalInfo>"
|
||||||
|
text = text[:empty.start()] + replacement + text[empty.end():]
|
||||||
|
elif opened:
|
||||||
|
if re.search(rf"<xr:Property>{re.escape(property_name)}</xr:Property>", opened.group(2)):
|
||||||
|
return
|
||||||
|
ind = opened.group(1)
|
||||||
|
block = build_property_state_xml(property_name, ind + "\t")
|
||||||
|
# Дописываем в КОНЕЦ InternalInfo: у Конфигуратора PropertyState идёт после GeneratedType.
|
||||||
|
close_at = opened.end() - len("</InternalInfo>") - len(ind)
|
||||||
|
text = text[:close_at] + block + nl + text[close_at:]
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(obj_file, "w", encoding="utf-8-sig", newline="") as fh:
|
||||||
|
fh.write(text)
|
||||||
|
|
||||||
|
|
||||||
def apply_pal_ns(format_version):
|
def apply_pal_ns(format_version):
|
||||||
"""2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
|
"""2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
|
||||||
Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
|
Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
|
||||||
@@ -501,6 +730,7 @@ def main():
|
|||||||
parser.add_argument("-ConfigPath", required=True)
|
parser.add_argument("-ConfigPath", required=True)
|
||||||
parser.add_argument("-Object", required=True)
|
parser.add_argument("-Object", required=True)
|
||||||
parser.add_argument("-BorrowMainAttribute", nargs="?", const="Form", default=None)
|
parser.add_argument("-BorrowMainAttribute", nargs="?", const="Form", default=None)
|
||||||
|
parser.add_argument("-Module", default=None)
|
||||||
args = ci_parse_args(parser)
|
args = ci_parse_args(parser)
|
||||||
|
|
||||||
# --- 1. Resolve paths ---
|
# --- 1. Resolve paths ---
|
||||||
@@ -582,6 +812,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 +894,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
|
||||||
@@ -656,6 +936,25 @@ def main():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return src_uuid
|
return src_uuid
|
||||||
|
|
||||||
|
# --- Пустой модуль заимствованного объекта ---
|
||||||
|
def new_borrowed_module_file(type_name, obj_name, module_kind):
|
||||||
|
dir_name = CHILD_TYPE_DIR_MAP[type_name]
|
||||||
|
module_dir = os.path.join(ext_dir, dir_name, obj_name, "Ext")
|
||||||
|
os.makedirs(module_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# NEVER overwrite an existing one: повторное заимствование не должно затирать дописанный
|
||||||
|
# код (то же правило, что у модуля формы).
|
||||||
|
module_file = os.path.join(module_dir, f"{module_kind}.bsl")
|
||||||
|
if os.path.isfile(module_file):
|
||||||
|
info(f" Preserved existing {module_kind}.bsl")
|
||||||
|
else:
|
||||||
|
write_utf8_bom(module_file, "")
|
||||||
|
info(f" Created: {module_file}")
|
||||||
|
|
||||||
|
# Флаг ставим и для уже существовавшего файла: состояние объекта должно отражать факт модуля.
|
||||||
|
set_property_state_flag(os.path.join(ext_dir, dir_name, f"{obj_name}.xml"), module_kind, format_version)
|
||||||
|
return module_file
|
||||||
|
|
||||||
def build_internal_info_xml(type_name, obj_name, indent):
|
def build_internal_info_xml(type_name, obj_name, indent):
|
||||||
types = GENERATED_TYPES.get(type_name)
|
types = GENERATED_TYPES.get(type_name)
|
||||||
if not types:
|
if not types:
|
||||||
@@ -703,6 +1002,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 +1112,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 +1174,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 +1190,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 +1204,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 +1272,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 +1291,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 +1351,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 +1372,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 +1384,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 +1459,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 +1488,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 +1521,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 +1547,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 +1561,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 +1751,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 +1827,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 +2020,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 +2052,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 +2070,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).
|
||||||
@@ -1696,6 +2114,47 @@ def main():
|
|||||||
print("-BorrowMainAttribute requires a form in -Object (e.g. 'Catalog.X.Form.Y')", file=sys.stderr)
|
print("-BorrowMainAttribute requires a form in -Object (e.g. 'Catalog.X.Form.Y')", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- 9c. Validate -Module ---
|
||||||
|
requested_modules = []
|
||||||
|
no_module = False
|
||||||
|
if args.Module:
|
||||||
|
for raw in re.split(r"[,;]", args.Module):
|
||||||
|
kind = raw.strip()
|
||||||
|
if not kind:
|
||||||
|
continue
|
||||||
|
# Сравнение РЕГИСТРОНЕЗАВИСИМОЕ явно: в ps1-порте `-ieq`, и молчаливое расхождение
|
||||||
|
# портов на «none» ловится только глазами.
|
||||||
|
if kind.lower() == "none":
|
||||||
|
no_module = True
|
||||||
|
continue
|
||||||
|
canon = [k for k in MODULE_KIND_NAMES if k.lower() == kind.lower()]
|
||||||
|
if not canon:
|
||||||
|
print(f"Неизвестный вид модуля '{kind}'. Допустимо: {', '.join(MODULE_KIND_NAMES)}, None", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
requested_modules.append(canon[0])
|
||||||
|
if no_module and requested_modules:
|
||||||
|
print("-Module None нельзя сочетать с видами модулей", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Какие модули создать для объекта. Тип с единственным модулем получает его всегда — уточнять
|
||||||
|
# там нечего; -Module разбирает только неоднозначные типы. Иначе батч смешанных типов
|
||||||
|
# (`CommonModule.X ;; Catalog.Y`) не выражался бы одним вызовом.
|
||||||
|
def resolve_module_kinds(type_name):
|
||||||
|
if no_module:
|
||||||
|
return []
|
||||||
|
allowed = MODULE_KINDS_BY_TYPE.get(type_name, [])
|
||||||
|
if not allowed:
|
||||||
|
return []
|
||||||
|
if type_name in AUTO_MODULE_TYPES:
|
||||||
|
return [allowed[0]]
|
||||||
|
if not requested_modules:
|
||||||
|
return []
|
||||||
|
# Порядок берём из таблицы типа, а не из порядка ключей в -Module.
|
||||||
|
selected = [k for k in allowed if k in requested_modules]
|
||||||
|
if not selected:
|
||||||
|
warn(f" Тип {type_name} не имеет запрошенных модулей — пропущено. Допустимо: {', '.join(allowed)}")
|
||||||
|
return selected
|
||||||
|
|
||||||
# --- 10. Process each item ---
|
# --- 10. Process each item ---
|
||||||
borrowed_count = 0
|
borrowed_count = 0
|
||||||
|
|
||||||
@@ -1747,6 +2206,9 @@ def main():
|
|||||||
has_bma = borrow_main_attribute_mode is not None
|
has_bma = borrow_main_attribute_mode is not None
|
||||||
form_files = borrow_form(type_name, obj_name, form_name, borrow_main_attr=has_bma)
|
form_files = borrow_form(type_name, obj_name, form_name, borrow_main_attr=has_bma)
|
||||||
borrowed_files.extend(form_files)
|
borrowed_files.extend(form_files)
|
||||||
|
# Замер на 8.3.26: платформа помечает форму расширенной сразу при заимствовании,
|
||||||
|
# даже если элементы не менялись. Флаг живёт в метаданных формы, не у владельца.
|
||||||
|
set_property_state_flag(form_files[0], "Form", format_version)
|
||||||
borrowed_count += 1
|
borrowed_count += 1
|
||||||
|
|
||||||
# Borrow main attribute if requested
|
# Borrow main attribute if requested
|
||||||
@@ -1754,25 +2216,75 @@ def main():
|
|||||||
borrow_main_attribute(type_name, obj_name, form_name, borrow_main_attribute_mode)
|
borrow_main_attribute(type_name, obj_name, form_name, borrow_main_attribute_mode)
|
||||||
else:
|
else:
|
||||||
# --- Object borrowing ---
|
# --- Object borrowing ---
|
||||||
info(f"Borrowing {type_name}.{obj_name}...")
|
|
||||||
|
|
||||||
src = read_source_object(type_name, obj_name)
|
|
||||||
info(f" Source UUID: {src['Uuid']}")
|
|
||||||
|
|
||||||
borrowed_xml = build_borrowed_object_xml(type_name, obj_name, src["Uuid"], src["Properties"])
|
|
||||||
|
|
||||||
target_dir = os.path.join(ext_dir, dir_name)
|
target_dir = os.path.join(ext_dir, dir_name)
|
||||||
os.makedirs(target_dir, exist_ok=True)
|
|
||||||
|
|
||||||
target_file = os.path.join(target_dir, f"{obj_name}.xml")
|
target_file = os.path.join(target_dir, f"{obj_name}.xml")
|
||||||
write_xml_file(target_file, borrowed_xml)
|
|
||||||
info(f" Created: {target_file}")
|
# Уже заимствованный объект НЕ переписываем: в его XML лежат собственные реквизиты
|
||||||
|
# расширения, заимствованные подобъекты и состояния, которые из источника не
|
||||||
|
# выводятся. Повторный вызов — законный способ доделать модуль (-Module), а не
|
||||||
|
# переиздать заготовку.
|
||||||
|
if test_object_borrowed(type_name, obj_name):
|
||||||
|
info(f"Already borrowed: {type_name}.{obj_name} — XML сохранён без изменений")
|
||||||
|
else:
|
||||||
|
info(f"Borrowing {type_name}.{obj_name}...")
|
||||||
|
|
||||||
|
src = read_source_object(type_name, obj_name)
|
||||||
|
info(f" Source UUID: {src['Uuid']}")
|
||||||
|
|
||||||
|
borrowed_xml = build_borrowed_object_xml(type_name, obj_name, src["Uuid"], src["Properties"])
|
||||||
|
|
||||||
|
os.makedirs(target_dir, exist_ok=True)
|
||||||
|
write_xml_file(target_file, borrowed_xml)
|
||||||
|
info(f" Created: {target_file}")
|
||||||
|
|
||||||
add_to_child_objects(type_name, obj_name)
|
add_to_child_objects(type_name, obj_name)
|
||||||
|
|
||||||
borrowed_files.append(target_file)
|
borrowed_files.append(target_file)
|
||||||
|
for kind in resolve_module_kinds(type_name):
|
||||||
|
borrowed_files.append(new_borrowed_module_file(type_name, obj_name, kind))
|
||||||
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
|
powershell.exe -NoProfile -File ".augment/skills/cfe-diff/scripts/cfe-diff.ps1" -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
|
||||||
```
|
```
|
||||||
+3
-2
@@ -1,7 +1,8 @@
|
|||||||
# cfe-diff v1.2 — Analyze and compare 1C configuration extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
|
# cfe-diff v1.3 — Analyze and compare 1C configuration extension (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory, Position=0)]
|
||||||
[string]$ExtensionPath,
|
[string]$ExtensionPath,
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-diff v1.2 — Analyze and compare 1C configuration extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
|
# cfe-diff v1.3 — Analyze and compare 1C configuration extension (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -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 "МоёРасширение"
|
powershell.exe -NoProfile -File ".augment/skills/cfe-init/scripts/cfe-init.ps1" -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>
|
||||||
```
|
```
|
||||||
|
|
||||||
+9
-9
@@ -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
|
powershell.exe -NoProfile -File ".augment/skills/cfe-patch-method/scripts/cfe-patch-method.ps1" -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>
|
||||||
```
|
```
|
||||||
+101
-1
@@ -1,4 +1,4 @@
|
|||||||
# cfe-patch-method v2.7 — Source-aware method interceptor for 1C extension (CFE) (+прощающий ввод: имя каталога наравне с именем типа)
|
# cfe-patch-method v2.8 — Source-aware method interceptor for 1C extension (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -788,6 +788,100 @@ if (Test-Path $ExtensionPath -PathType Leaf) { $ExtensionPath = Split-Path $Exte
|
|||||||
$cfgFile = Join-Path $ExtensionPath "Configuration.xml"
|
$cfgFile = Join-Path $ExtensionPath "Configuration.xml"
|
||||||
if (-not (Test-Path $cfgFile)) { Write-Error "Configuration.xml не найден в расширении: $ExtensionPath"; exit 1 }
|
if (-not (Test-Path $cfgFile)) { Write-Error "Configuration.xml не найден в расширении: $ExtensionPath"; exit 1 }
|
||||||
|
|
||||||
|
# --- Пометка расширенного свойства (<xr:PropertyState>) ---
|
||||||
|
# Свойство появилось в формате 2.19 (8.3.26): на 2.18 и ниже платформа молча выбрасывает элемент
|
||||||
|
# при загрузке. С 2.19 Конфигуратор ставит его сам при выгрузке. Правило: флаг ставит тот, кто
|
||||||
|
# создал файл модуля, — здесь это мы. Имя свойства = базовое имя файла модуля.
|
||||||
|
# Копии этих функций есть в cfe-borrow (навыки автономны); держать их одинаковыми — сознательно.
|
||||||
|
function Get-FormatRank([string]$ver) {
|
||||||
|
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Build-PropertyStateXml {
|
||||||
|
param([string]$propertyName, [string]$indent)
|
||||||
|
|
||||||
|
$sb = New-Object System.Text.StringBuilder
|
||||||
|
$sb.AppendLine("${indent}<xr:PropertyState>") | Out-Null
|
||||||
|
$sb.AppendLine("${indent}`t<xr:Property>${propertyName}</xr:Property>") | Out-Null
|
||||||
|
$sb.AppendLine("${indent}`t<xr:State>Extended</xr:State>") | Out-Null
|
||||||
|
$sb.Append("${indent}</xr:PropertyState>") | Out-Null
|
||||||
|
return $sb.ToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-PropertyStateFlag {
|
||||||
|
param([string]$objFile, [string]$propertyName, [string]$formatVersion)
|
||||||
|
|
||||||
|
if ((Get-FormatRank $formatVersion) -lt 219) { return }
|
||||||
|
if (-not (Test-Path $objFile)) { return }
|
||||||
|
|
||||||
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
$text = [System.IO.File]::ReadAllText($objFile, $enc)
|
||||||
|
$nl = if ($text -match "`r`n") { "`r`n" } else { "`n" }
|
||||||
|
|
||||||
|
# ПЕРВЫЙ <InternalInfo> в файле — собственный у объекта: у реквизитов и подобъектов свои,
|
||||||
|
# но они лежат ниже, внутри <ChildObjects>.
|
||||||
|
$empty = [regex]::Match($text, '([ \t]*)<InternalInfo\s*/>')
|
||||||
|
$open = [regex]::Match($text, '(?s)([ \t]*)<InternalInfo>(.*?)</InternalInfo>')
|
||||||
|
|
||||||
|
if ($empty.Success -and (-not $open.Success -or $empty.Index -lt $open.Index)) {
|
||||||
|
$ind = $empty.Groups[1].Value
|
||||||
|
$block = Build-PropertyStateXml $propertyName ($ind + "`t")
|
||||||
|
$replacement = "${ind}<InternalInfo>${nl}${block}${nl}${ind}</InternalInfo>"
|
||||||
|
$text = $text.Remove($empty.Index, $empty.Length).Insert($empty.Index, $replacement)
|
||||||
|
} elseif ($open.Success) {
|
||||||
|
if ($open.Groups[2].Value -match "<xr:Property>$([regex]::Escape($propertyName))</xr:Property>") { return }
|
||||||
|
$ind = $open.Groups[1].Value
|
||||||
|
$block = Build-PropertyStateXml $propertyName ($ind + "`t")
|
||||||
|
# Дописываем в КОНЕЦ InternalInfo: у Конфигуратора PropertyState идёт после GeneratedType.
|
||||||
|
$closeAt = $open.Index + $open.Length - "</InternalInfo>".Length - $ind.Length
|
||||||
|
$text = $text.Insert($closeAt, "${block}${nl}")
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
[System.IO.File]::WriteAllText($objFile, $text, $enc)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Модуль формы сюда не попадает: у формы флаг называется Form и ставится при заимствовании,
|
||||||
|
# а не при появлении модуля (замер на 8.3.26 — пустой модуль формы платформа не выгружает).
|
||||||
|
function Get-ModuleFlagTarget {
|
||||||
|
param([string[]]$relParts, [string]$extRoot)
|
||||||
|
|
||||||
|
if ($relParts.Count -ne 4 -or $relParts[2] -ne "Ext") { return $null }
|
||||||
|
$prop = [System.IO.Path]::GetFileNameWithoutExtension($relParts[3])
|
||||||
|
return @{
|
||||||
|
File = (Join-Path (Join-Path $extRoot $relParts[0]) "$($relParts[1]).xml")
|
||||||
|
Property = $prop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# --- Read NamePrefix ---
|
# --- Read NamePrefix ---
|
||||||
$cfgDoc = New-Object System.Xml.XmlDocument
|
$cfgDoc = New-Object System.Xml.XmlDocument
|
||||||
$cfgDoc.PreserveWhitespace = $false
|
$cfgDoc.PreserveWhitespace = $false
|
||||||
@@ -1084,6 +1178,12 @@ if ($reuseRegionIdx -ge 0) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Модуль в расширении есть — отражаем это в метаданных объекта (формат ≥ 2.19).
|
||||||
|
$flagTarget = Get-ModuleFlagTarget $relParts $ExtensionPath
|
||||||
|
if ($flagTarget) {
|
||||||
|
Set-PropertyStateFlag $flagTarget.File $flagTarget.Property (Detect-FormatVersion $ExtensionPath)
|
||||||
|
}
|
||||||
|
|
||||||
Write-Host "[OK] Перехватчик &$decoratorRu(`"$MethodName`") — $placement"
|
Write-Host "[OK] Перехватчик &$decoratorRu(`"$MethodName`") — $placement"
|
||||||
Write-Host " Файл: $extBsl"
|
Write-Host " Файл: $extBsl"
|
||||||
Write-Host " Процедура: $interceptorName($($method.ParamsText -replace '\s+', ' '))"
|
Write-Host " Процедура: $interceptorName($($method.ParamsText -replace '\s+', ' '))"
|
||||||
+100
-1
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-patch-method v2.7 — Source-aware method interceptor for 1C extension (CFE) (+прощающий ввод: имя каталога наравне с именем типа)
|
# cfe-patch-method v2.8 — Source-aware method interceptor for 1C extension (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -75,6 +75,99 @@ CONTEXT_RE = re.compile(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Пометка расширенного свойства (<xr:PropertyState>) ---
|
||||||
|
# Свойство появилось в формате 2.19 (8.3.26): на 2.18 и ниже платформа молча выбрасывает элемент
|
||||||
|
# при загрузке. С 2.19 Конфигуратор ставит его сам при выгрузке. Правило: флаг ставит тот, кто
|
||||||
|
# создал файл модуля, — здесь это мы. Имя свойства = базовое имя файла модуля.
|
||||||
|
# Копии этих функций есть в cfe-borrow (навыки автономны); держать их одинаковыми — сознательно.
|
||||||
|
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 build_property_state_xml(property_name, indent):
|
||||||
|
return "\n".join([
|
||||||
|
f"{indent}<xr:PropertyState>",
|
||||||
|
f"{indent}\t<xr:Property>{property_name}</xr:Property>",
|
||||||
|
f"{indent}\t<xr:State>Extended</xr:State>",
|
||||||
|
f"{indent}</xr:PropertyState>",
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def set_property_state_flag(obj_file, property_name, format_version):
|
||||||
|
if format_rank(format_version) < 219:
|
||||||
|
return
|
||||||
|
if not os.path.isfile(obj_file):
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(obj_file, "r", encoding="utf-8-sig", newline="") as fh:
|
||||||
|
text = fh.read()
|
||||||
|
nl = "\r\n" if "\r\n" in text else "\n"
|
||||||
|
|
||||||
|
# ПЕРВЫЙ <InternalInfo> в файле — собственный у объекта: у реквизитов и подобъектов свои,
|
||||||
|
# но они лежат ниже, внутри <ChildObjects>.
|
||||||
|
empty = re.search(r"([ \t]*)<InternalInfo\s*/>", text)
|
||||||
|
opened = re.search(r"([ \t]*)<InternalInfo>(.*?)</InternalInfo>", text, re.S)
|
||||||
|
|
||||||
|
if empty and (not opened or empty.start() < opened.start()):
|
||||||
|
ind = empty.group(1)
|
||||||
|
block = build_property_state_xml(property_name, ind + "\t")
|
||||||
|
replacement = f"{ind}<InternalInfo>{nl}{block}{nl}{ind}</InternalInfo>"
|
||||||
|
text = text[:empty.start()] + replacement + text[empty.end():]
|
||||||
|
elif opened:
|
||||||
|
if re.search(rf"<xr:Property>{re.escape(property_name)}</xr:Property>", opened.group(2)):
|
||||||
|
return
|
||||||
|
ind = opened.group(1)
|
||||||
|
block = build_property_state_xml(property_name, ind + "\t")
|
||||||
|
# Дописываем в КОНЕЦ InternalInfo: у Конфигуратора PropertyState идёт после GeneratedType.
|
||||||
|
close_at = opened.end() - len("</InternalInfo>") - len(ind)
|
||||||
|
text = text[:close_at] + block + nl + text[close_at:]
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(obj_file, "w", encoding="utf-8-sig", newline="") as fh:
|
||||||
|
fh.write(text)
|
||||||
|
|
||||||
|
|
||||||
|
# Модуль формы сюда не попадает: у формы флаг называется Form и ставится при заимствовании,
|
||||||
|
# а не при появлении модуля (замер на 8.3.26 — пустой модуль формы платформа не выгружает).
|
||||||
|
def get_module_flag_target(rel_parts, ext_root):
|
||||||
|
if len(rel_parts) != 4 or rel_parts[2] != "Ext":
|
||||||
|
return None
|
||||||
|
prop = os.path.splitext(rel_parts[3])[0]
|
||||||
|
return {
|
||||||
|
"file": os.path.join(ext_root, rel_parts[0], f"{rel_parts[1]}.xml"),
|
||||||
|
"property": prop,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_module_rel_path(module_path):
|
def get_module_rel_path(module_path):
|
||||||
parts = module_path.split(".")
|
parts = module_path.split(".")
|
||||||
if len(parts) < 2:
|
if len(parts) < 2:
|
||||||
@@ -841,6 +934,12 @@ def main():
|
|||||||
|
|
||||||
place_new(ext_bsl, ext_lines, ext_exists, method["chain"], core)
|
place_new(ext_bsl, ext_lines, ext_exists, method["chain"], core)
|
||||||
|
|
||||||
|
# Модуль в расширении есть — отражаем это в метаданных объекта (формат ≥ 2.19).
|
||||||
|
flag_target = get_module_flag_target(rel_parts, extension_path)
|
||||||
|
if flag_target:
|
||||||
|
set_property_state_flag(flag_target["file"], flag_target["property"],
|
||||||
|
detect_format_version(extension_path))
|
||||||
|
|
||||||
# emit summary
|
# emit summary
|
||||||
placement = place_new.placement
|
placement = place_new.placement
|
||||||
print('[OK] Перехватчик &%s("%s") — %s' % (decorator_ru, method_name, placement))
|
print('[OK] Перехватчик &%s("%s") — %s' % (decorator_ru, method_name, placement))
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
name: cfe-validate
|
||||||
|
description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности
|
||||||
|
argument-hint: <ExtensionPath> [-ConfigPath <ConfigDir>] [-Detailed] [-MaxErrors 30]
|
||||||
|
allowed-tools:
|
||||||
|
- Bash
|
||||||
|
- Read
|
||||||
|
- Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
# /cfe-validate — валидация расширения конфигурации (CFE)
|
||||||
|
|
||||||
|
Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты, права ролей. Аналог `/cf-validate`, но для расширений.
|
||||||
|
|
||||||
|
## Параметры
|
||||||
|
|
||||||
|
| Параметр | Обяз. | Умолч. | Описание |
|
||||||
|
|---------------|:-----:|---------|-------------------------------------------------|
|
||||||
|
| ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения |
|
||||||
|
| ConfigPath | нет | — | Каталог конфигурации, из которой заимствованы объекты |
|
||||||
|
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
|
||||||
|
| MaxErrors | нет | 30 | Остановиться после N ошибок |
|
||||||
|
| OutFile | нет | — | Записать результат в файл |
|
||||||
|
|
||||||
|
### ConfigPath
|
||||||
|
|
||||||
|
Указывай всегда, когда конфигурация-источник доступна: без неё часть ошибок заимствованных форм не ловится, и расширение может пройти валидацию, а потом быть отвергнутым платформой при загрузке.
|
||||||
|
|
||||||
|
Если пользователь не указал путь — определи сам:
|
||||||
|
1. Прочитай `.v8-project.json` из корня проекта
|
||||||
|
2. Разреши целевую базу (по имени, ветке или `default`)
|
||||||
|
3. Возьми её поле `configSrc`
|
||||||
|
|
||||||
|
## Команда
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell.exe -NoProfile -File ".augment/skills/cfe-validate/scripts/cfe-validate.ps1" -ExtensionPath "src\cfe\extname"
|
||||||
|
powershell.exe -NoProfile -File ".augment/skills/cfe-validate/scripts/cfe-validate.ps1" -ExtensionPath "src\cfe\extname\Configuration.xml"
|
||||||
|
powershell.exe -NoProfile -File ".augment/skills/cfe-validate/scripts/cfe-validate.ps1" -ExtensionPath "src\cfe\extname" -ConfigPath "src\cf"
|
||||||
|
```
|
||||||
+374
-7
@@ -1,7 +1,8 @@
|
|||||||
# cfe-validate v1.7 — Validate 1C configuration extension structure (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
|
# cfe-validate v1.14 — Validate 1C configuration extension structure (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory, Position=0)]
|
||||||
[Alias('Path')]
|
[Alias('Path')]
|
||||||
[string]$ExtensionPath,
|
[string]$ExtensionPath,
|
||||||
|
|
||||||
@@ -9,7 +10,11 @@ param(
|
|||||||
|
|
||||||
[int]$MaxErrors = 30,
|
[int]$MaxErrors = 30,
|
||||||
|
|
||||||
[string]$OutFile
|
[string]$OutFile,
|
||||||
|
|
||||||
|
# Конфигурация-источник. Без неё проверки, требующие сравнения с основной конфигурацией,
|
||||||
|
# пропускаются (о чём сказано в отчёте), остальные работают как раньше.
|
||||||
|
[string]$ConfigPath
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
@@ -89,8 +94,42 @@ $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}$'
|
# Модули заимствованных объектов: тип → виды модулей. Имя свойства в <xr:PropertyState>
|
||||||
|
# совпадает с базовым именем файла модуля. Копия таблицы есть в cfe-borrow (навыки автономны).
|
||||||
|
$moduleKindsByType = @{
|
||||||
|
"CommonModule"=@("Module"); "HTTPService"=@("Module"); "WebService"=@("Module")
|
||||||
|
"Catalog"=@("ObjectModule","ManagerModule"); "Document"=@("ObjectModule","ManagerModule")
|
||||||
|
"Report"=@("ObjectModule","ManagerModule"); "DataProcessor"=@("ObjectModule","ManagerModule")
|
||||||
|
"ExchangePlan"=@("ObjectModule","ManagerModule")
|
||||||
|
"ChartOfCharacteristicTypes"=@("ObjectModule","ManagerModule")
|
||||||
|
"ChartOfAccounts"=@("ObjectModule","ManagerModule")
|
||||||
|
"ChartOfCalculationTypes"=@("ObjectModule","ManagerModule")
|
||||||
|
"BusinessProcess"=@("ObjectModule","ManagerModule"); "Task"=@("ObjectModule","ManagerModule")
|
||||||
|
"InformationRegister"=@("RecordSetModule","ManagerModule")
|
||||||
|
"AccumulationRegister"=@("RecordSetModule","ManagerModule")
|
||||||
|
"AccountingRegister"=@("RecordSetModule","ManagerModule")
|
||||||
|
"CalculationRegister"=@("RecordSetModule","ManagerModule")
|
||||||
|
"Sequence"=@("RecordSetModule","ManagerModule")
|
||||||
|
"Constant"=@("ValueManagerModule","ManagerModule")
|
||||||
|
"Enum"=@("ManagerModule"); "DocumentJournal"=@("ManagerModule")
|
||||||
|
"FilterCriterion"=@("ManagerModule")
|
||||||
|
}
|
||||||
|
|
||||||
|
$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_]*$'
|
||||||
|
|
||||||
# 7 fixed ClassIds for Configuration
|
# 7 fixed ClassIds for Configuration
|
||||||
@@ -144,6 +183,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 +274,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 +620,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 +724,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 +770,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 +1005,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 +1072,232 @@ 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Check 15: основные роли расширения не дают прав на заимствованные объекты ---
|
||||||
|
# Платформа: «Назначение прав доступа на заимствованные объекты основными ролями в
|
||||||
|
# расширениях недопустимо». Роль вне <DefaultRoles> так делать вправе — проверяем только
|
||||||
|
# основные. Ловится статически, а по симптому (отказ загрузки) причина не читается.
|
||||||
|
$defaultRoleNodes = @($cfgNode.SelectNodes("md:Properties/md:DefaultRoles/xr:Item", $ns))
|
||||||
|
if ($defaultRoleNodes.Count -gt 0) {
|
||||||
|
$adoptedCache = @{}
|
||||||
|
|
||||||
|
function Test-ObjectAdopted {
|
||||||
|
param([string]$typeName, [string]$objName)
|
||||||
|
$key = "$typeName.$objName"
|
||||||
|
if ($adoptedCache.ContainsKey($key)) { return $adoptedCache[$key] }
|
||||||
|
$adoptedCache[$key] = $false
|
||||||
|
if ($childTypeDirMap.ContainsKey($typeName)) {
|
||||||
|
$objPath = Join-Path (Join-Path $configDir $childTypeDirMap[$typeName]) "$objName.xml"
|
||||||
|
if (Test-Path $objPath) {
|
||||||
|
try {
|
||||||
|
$objDoc = New-Object System.Xml.XmlDocument
|
||||||
|
$objDoc.Load($objPath)
|
||||||
|
$objNs = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable)
|
||||||
|
$objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||||
|
$ob = $objDoc.SelectSingleNode("/md:MetaDataObject/md:$typeName/md:Properties/md:ObjectBelonging", $objNs)
|
||||||
|
if ($ob -and $ob.InnerText -eq "Adopted") { $adoptedCache[$key] = $true }
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $adoptedCache[$key]
|
||||||
|
}
|
||||||
|
|
||||||
|
$check15Ok = $true
|
||||||
|
$check15Count = 0
|
||||||
|
foreach ($rn in $defaultRoleNodes) {
|
||||||
|
$roleRef = $rn.InnerText
|
||||||
|
if ($roleRef -notmatch '^Role\.(.+)$') { continue }
|
||||||
|
$defRoleName = $Matches[1]
|
||||||
|
$rightsPath = Join-Path (Join-Path (Join-Path $configDir "Roles") $defRoleName) "Ext\Rights.xml"
|
||||||
|
if (-not (Test-Path $rightsPath)) { continue }
|
||||||
|
try {
|
||||||
|
$rDoc = New-Object System.Xml.XmlDocument
|
||||||
|
$rDoc.Load($rightsPath)
|
||||||
|
} catch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$rNs = New-Object System.Xml.XmlNamespaceManager($rDoc.NameTable)
|
||||||
|
$rNs.AddNamespace("r", "http://v8.1c.ru/8.2/roles")
|
||||||
|
foreach ($nameNode in $rDoc.SelectNodes("/r:Rights/r:object/r:name", $rNs)) {
|
||||||
|
$fullName = $nameNode.InnerText
|
||||||
|
$segs = $fullName.Split(".")
|
||||||
|
# Configuration.* — права самого расширения, не объект; заимствования там нет.
|
||||||
|
if ($segs.Count -lt 2 -or $segs[0] -eq "Configuration") { continue }
|
||||||
|
$check15Count++
|
||||||
|
if (Test-ObjectAdopted $segs[0] $segs[1]) {
|
||||||
|
Report-Error ("15. Роль '$defRoleName' входит в DefaultRoles и даёт права на заимствованный $($segs[0]).$($segs[1]) " +
|
||||||
|
"($fullName): платформа это запрещает. Вынесите такие права в отдельную роль вне DefaultRoles.")
|
||||||
|
$check15Ok = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($check15Ok -and $check15Count -gt 0) {
|
||||||
|
Report-OK "15. Основные роли: прав на заимствованные объекты нет ($check15Count checked)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($script:stopped) { & $finalize; exit 1 }
|
||||||
|
|
||||||
|
# --- Check 16: модуль заимствованного объекта и пометка расширенного свойства ---
|
||||||
|
# Свойство <xr:PropertyState> появилось в формате 2.19 (8.3.26); ниже платформа его молча
|
||||||
|
# выбрасывает, поэтому там проверять нечего. С 2.19 состояние обязано соответствовать факту:
|
||||||
|
# есть файл модуля — есть пометка, и наоборот. Перекос платформа принимает (проверено на стенде),
|
||||||
|
# но выгрузка Конфигуратора так не выглядит — отсюда предупреждение, а не ошибка.
|
||||||
|
if ($versionRank -ge 219 -and $childObjNode) {
|
||||||
|
$stateIssues = @()
|
||||||
|
$stateChecked = 0
|
||||||
|
foreach ($child in $childObjNode.ChildNodes) {
|
||||||
|
if ($child.NodeType -ne 'Element') { continue }
|
||||||
|
$typeName = $child.LocalName
|
||||||
|
if (-not $moduleKindsByType.ContainsKey($typeName)) { continue }
|
||||||
|
if (-not $childTypeDirMap.ContainsKey($typeName)) { continue }
|
||||||
|
$stateObjName = $child.InnerText.Trim()
|
||||||
|
if (-not $stateObjName) { continue }
|
||||||
|
$typeDir = Join-Path $configDir $childTypeDirMap[$typeName]
|
||||||
|
$objFile = Join-Path $typeDir "$stateObjName.xml"
|
||||||
|
if (-not (Test-Path $objFile)) { continue }
|
||||||
|
$objText = [System.IO.File]::ReadAllText($objFile, [System.Text.Encoding]::UTF8)
|
||||||
|
if ($objText -notmatch '<ObjectBelonging>Adopted</ObjectBelonging>') { continue }
|
||||||
|
|
||||||
|
foreach ($kind in $moduleKindsByType[$typeName]) {
|
||||||
|
$stateChecked++
|
||||||
|
$hasFile = Test-Path (Join-Path (Join-Path (Join-Path $typeDir $stateObjName) "Ext") "$kind.bsl")
|
||||||
|
$hasFlag = $objText -match "<xr:Property>$kind</xr:Property>"
|
||||||
|
if ($hasFile -and -not $hasFlag) {
|
||||||
|
$stateIssues += "$typeName.$stateObjName — есть $kind.bsl, но нет <xr:PropertyState> для $kind"
|
||||||
|
} elseif ($hasFlag -and -not $hasFile) {
|
||||||
|
$stateIssues += "$typeName.$stateObjName — есть <xr:PropertyState> для $kind, но нет $kind.bsl"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($stateChecked -gt 0) {
|
||||||
|
if ($stateIssues.Count -eq 0) {
|
||||||
|
Report-OK "16. Модули заимствованных объектов: пометки расширенных свойств согласованы ($stateChecked)"
|
||||||
|
} else {
|
||||||
|
foreach ($issue in $stateIssues) { Report-Warn "16. $issue" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# --- 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
|
||||||
+359
-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.14 — Validate 1C configuration extension XML structure (CFE)
|
||||||
# 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
|
||||||
@@ -71,6 +71,27 @@ CHILD_OBJECT_TYPES = [
|
|||||||
'BusinessProcess', 'Task', 'IntegrationService',
|
'BusinessProcess', 'Task', 'IntegrationService',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Модули заимствованных объектов: тип → виды модулей. Имя свойства в <xr:PropertyState>
|
||||||
|
# совпадает с базовым именем файла модуля. Копия таблицы есть в cfe-borrow (навыки автономны).
|
||||||
|
MODULE_KINDS_BY_TYPE = {
|
||||||
|
"CommonModule": ["Module"], "HTTPService": ["Module"], "WebService": ["Module"],
|
||||||
|
"Catalog": ["ObjectModule", "ManagerModule"], "Document": ["ObjectModule", "ManagerModule"],
|
||||||
|
"Report": ["ObjectModule", "ManagerModule"], "DataProcessor": ["ObjectModule", "ManagerModule"],
|
||||||
|
"ExchangePlan": ["ObjectModule", "ManagerModule"],
|
||||||
|
"ChartOfCharacteristicTypes": ["ObjectModule", "ManagerModule"],
|
||||||
|
"ChartOfAccounts": ["ObjectModule", "ManagerModule"],
|
||||||
|
"ChartOfCalculationTypes": ["ObjectModule", "ManagerModule"],
|
||||||
|
"BusinessProcess": ["ObjectModule", "ManagerModule"], "Task": ["ObjectModule", "ManagerModule"],
|
||||||
|
"InformationRegister": ["RecordSetModule", "ManagerModule"],
|
||||||
|
"AccumulationRegister": ["RecordSetModule", "ManagerModule"],
|
||||||
|
"AccountingRegister": ["RecordSetModule", "ManagerModule"],
|
||||||
|
"CalculationRegister": ["RecordSetModule", "ManagerModule"],
|
||||||
|
"Sequence": ["RecordSetModule", "ManagerModule"],
|
||||||
|
"Constant": ["ValueManagerModule", "ManagerModule"],
|
||||||
|
"Enum": ["ManagerModule"], "DocumentJournal": ["ManagerModule"],
|
||||||
|
"FilterCriterion": ["ManagerModule"],
|
||||||
|
}
|
||||||
|
|
||||||
# Type -> directory mapping
|
# Type -> directory mapping
|
||||||
CHILD_TYPE_DIR_MAP = {
|
CHILD_TYPE_DIR_MAP = {
|
||||||
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
||||||
@@ -96,6 +117,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 +182,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 +256,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 +320,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 +649,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 +749,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 +793,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 +988,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 +1042,228 @@ 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')
|
||||||
|
|
||||||
|
# --- Check 15: основные роли расширения не дают прав на заимствованные объекты ---
|
||||||
|
# Платформа: «Назначение прав доступа на заимствованные объекты основными ролями в
|
||||||
|
# расширениях недопустимо». Роль вне <DefaultRoles> так делать вправе — проверяем только
|
||||||
|
# основные. Ловится статически, а по симптому (отказ загрузки) причина не читается.
|
||||||
|
default_role_nodes = cfg_node.findall('md:Properties/md:DefaultRoles/xr:Item', NS)
|
||||||
|
if default_role_nodes:
|
||||||
|
adopted_cache = {}
|
||||||
|
|
||||||
|
def is_object_adopted(type_name, obj_name):
|
||||||
|
key = f"{type_name}.{obj_name}"
|
||||||
|
if key in adopted_cache:
|
||||||
|
return adopted_cache[key]
|
||||||
|
adopted_cache[key] = False
|
||||||
|
dir_name = CHILD_TYPE_DIR_MAP.get(type_name)
|
||||||
|
if dir_name:
|
||||||
|
obj_path = os.path.join(config_dir, dir_name, obj_name + '.xml')
|
||||||
|
if os.path.isfile(obj_path):
|
||||||
|
try:
|
||||||
|
obj_root = etree.parse(obj_path).getroot()
|
||||||
|
ob = obj_root.find(f'md:{type_name}/md:Properties/md:ObjectBelonging', NS)
|
||||||
|
if ob is not None and (ob.text or '') == 'Adopted':
|
||||||
|
adopted_cache[key] = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return adopted_cache[key]
|
||||||
|
|
||||||
|
check15_ok = True
|
||||||
|
check15_count = 0
|
||||||
|
roles_ns = {'r': 'http://v8.1c.ru/8.2/roles'}
|
||||||
|
for rn in default_role_nodes:
|
||||||
|
m = re.match(r'^Role\.(.+)$', rn.text or '')
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
def_role_name = m.group(1)
|
||||||
|
rights_path = os.path.join(config_dir, 'Roles', def_role_name, 'Ext', 'Rights.xml')
|
||||||
|
if not os.path.isfile(rights_path):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
rights_root = etree.parse(rights_path).getroot()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
for name_node in rights_root.findall('r:object/r:name', roles_ns):
|
||||||
|
full_name = name_node.text or ''
|
||||||
|
segs = full_name.split('.')
|
||||||
|
# Configuration.* — права самого расширения, не объект; заимствования там нет.
|
||||||
|
if len(segs) < 2 or segs[0] == 'Configuration':
|
||||||
|
continue
|
||||||
|
check15_count += 1
|
||||||
|
if is_object_adopted(segs[0], segs[1]):
|
||||||
|
r.error(f"15. Роль '{def_role_name}' входит в DefaultRoles и даёт права на заимствованный "
|
||||||
|
f"{segs[0]}.{segs[1]} ({full_name}): платформа это запрещает. "
|
||||||
|
"Вынесите такие права в отдельную роль вне DefaultRoles.")
|
||||||
|
check15_ok = False
|
||||||
|
if check15_ok and check15_count > 0:
|
||||||
|
r.ok(f'15. Основные роли: прав на заимствованные объекты нет ({check15_count} checked)')
|
||||||
|
|
||||||
|
if r.stopped:
|
||||||
|
r.finalize(out_file)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Check 16: модуль заимствованного объекта и пометка расширенного свойства ---
|
||||||
|
# Свойство <xr:PropertyState> появилось в формате 2.19 (8.3.26); ниже платформа его молча
|
||||||
|
# выбрасывает, поэтому там проверять нечего. С 2.19 состояние обязано соответствовать факту:
|
||||||
|
# есть файл модуля — есть пометка, и наоборот. Перекос платформа принимает (проверено на
|
||||||
|
# стенде), но выгрузка Конфигуратора так не выглядит — отсюда предупреждение, а не ошибка.
|
||||||
|
if version_rank >= 219 and child_obj_node is not None:
|
||||||
|
state_issues = []
|
||||||
|
state_checked = 0
|
||||||
|
for child in child_obj_node:
|
||||||
|
if not isinstance(child.tag, str):
|
||||||
|
continue
|
||||||
|
type_name = etree.QName(child.tag).localname
|
||||||
|
if type_name not in MODULE_KINDS_BY_TYPE or type_name not in CHILD_TYPE_DIR_MAP:
|
||||||
|
continue
|
||||||
|
obj_name_val = (child.text or '').strip()
|
||||||
|
if not obj_name_val:
|
||||||
|
continue
|
||||||
|
type_dir = os.path.join(config_dir, CHILD_TYPE_DIR_MAP[type_name])
|
||||||
|
obj_file = os.path.join(type_dir, f'{obj_name_val}.xml')
|
||||||
|
if not os.path.isfile(obj_file):
|
||||||
|
continue
|
||||||
|
with open(obj_file, 'r', encoding='utf-8-sig') as f:
|
||||||
|
obj_text = f.read()
|
||||||
|
if '<ObjectBelonging>Adopted</ObjectBelonging>' not in obj_text:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for kind in MODULE_KINDS_BY_TYPE[type_name]:
|
||||||
|
state_checked += 1
|
||||||
|
has_file = os.path.isfile(os.path.join(type_dir, obj_name_val, 'Ext', f'{kind}.bsl'))
|
||||||
|
has_flag = f'<xr:Property>{kind}</xr:Property>' in obj_text
|
||||||
|
if has_file and not has_flag:
|
||||||
|
state_issues.append(f'{type_name}.{obj_name_val} — есть {kind}.bsl, но нет <xr:PropertyState> для {kind}')
|
||||||
|
elif has_flag and not has_file:
|
||||||
|
state_issues.append(f'{type_name}.{obj_name_val} — есть <xr:PropertyState> для {kind}, но нет {kind}.bsl')
|
||||||
|
|
||||||
|
if state_checked > 0:
|
||||||
|
if not state_issues:
|
||||||
|
r.ok(f'16. Модули заимствованных объектов: пометки расширенных свойств согласованы ({state_checked})')
|
||||||
|
else:
|
||||||
|
for issue in state_issues:
|
||||||
|
r.warn(f'16. {issue}')
|
||||||
|
|
||||||
# --- 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" <параметры>
|
powershell.exe -NoProfile -File ".augment/skills/db-create/scripts/db-create.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -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"
|
powershell.exe -NoProfile -File ".augment/skills/db-create/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB"
|
||||||
|
|
||||||
# Создать серверную базу
|
# Создать серверную базу
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
|
powershell.exe -NoProfile -File ".augment/skills/db-create/scripts/db-create.ps1" -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"
|
powershell.exe -NoProfile -File ".augment/skills/db-create/scripts/db-create.ps1" -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 "Новая база"
|
powershell.exe -NoProfile -File ".augment/skills/db-create/scripts/db-create.ps1" -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" <параметры>
|
powershell.exe -NoProfile -File ".augment/skills/db-dump-cf/scripts/db-dump-cf.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -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"
|
powershell.exe -NoProfile -File ".augment/skills/db-dump-cf/scripts/db-dump-cf.ps1" -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"
|
powershell.exe -NoProfile -File ".augment/skills/db-dump-cf/scripts/db-dump-cf.ps1" -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 "МоёРасширение"
|
powershell.exe -NoProfile -File ".augment/skills/db-dump-cf/scripts/db-dump-cf.ps1" -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" <параметры>
|
powershell.exe -NoProfile -File ".augment/skills/db-dump-dt/scripts/db-dump-dt.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -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"
|
powershell.exe -NoProfile -File ".augment/skills/db-dump-dt/scripts/db-dump-dt.ps1" -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"
|
powershell.exe -NoProfile -File ".augment/skills/db-dump-dt/scripts/db-dump-dt.ps1" -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" <параметры>
|
powershell.exe -NoProfile -File ".augment/skills/db-dump-xml/scripts/db-dump-xml.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -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
|
powershell.exe -NoProfile -File ".augment/skills/db-dump-xml/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
|
||||||
|
|
||||||
# Инкрементальная выгрузка
|
# Инкрементальная выгрузка
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
|
powershell.exe -NoProfile -File ".augment/skills/db-dump-xml/scripts/db-dump-xml.ps1" -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 "Справочник.Номенклатура,Документ.Заказ"
|
powershell.exe -NoProfile -File ".augment/skills/db-dump-xml/scripts/db-dump-xml.ps1" -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
|
powershell.exe -NoProfile -File ".augment/skills/db-dump-xml/scripts/db-dump-xml.ps1" -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 "МоёРасширение"
|
powershell.exe -NoProfile -File ".augment/skills/db-dump-xml/scripts/db-dump-xml.ps1" -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" <параметры>
|
powershell.exe -NoProfile -File ".augment/skills/db-load-cf/scripts/db-load-cf.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -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"
|
powershell.exe -NoProfile -File ".augment/skills/db-load-cf/scripts/db-load-cf.ps1" -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"
|
powershell.exe -NoProfile -File ".augment/skills/db-load-cf/scripts/db-load-cf.ps1" -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 "МоёРасширение"
|
powershell.exe -NoProfile -File ".augment/skills/db-load-cf/scripts/db-load-cf.ps1" -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" <параметры>
|
powershell.exe -NoProfile -File ".augment/skills/db-load-dt/scripts/db-load-dt.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -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"
|
powershell.exe -NoProfile -File ".augment/skills/db-load-dt/scripts/db-load-dt.ps1" -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
|
powershell.exe -NoProfile -File ".augment/skills/db-load-dt/scripts/db-load-dt.ps1" -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" <параметры>
|
powershell.exe -NoProfile -File ".augment/skills/db-load-git/scripts/db-load-git.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -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
|
powershell.exe -NoProfile -File ".augment/skills/db-load-git/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
|
||||||
|
|
||||||
# Из диапазона коммитов
|
# Из диапазона коммитов
|
||||||
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"
|
powershell.exe -NoProfile -File ".augment/skills/db-load-git/scripts/db-load-git.ps1" -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" <параметры>
|
powershell.exe -NoProfile -File ".augment/skills/db-load-xml/scripts/db-load-xml.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -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
|
powershell.exe -NoProfile -File ".augment/skills/db-load-xml/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
|
||||||
|
|
||||||
# Частичная загрузка конкретных файлов
|
# Частичная загрузка конкретных файлов
|
||||||
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"
|
powershell.exe -NoProfile -File ".augment/skills/db-load-xml/scripts/db-load-xml.ps1" -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 "МоёРасширение"
|
powershell.exe -NoProfile -File ".augment/skills/db-load-xml/scripts/db-load-xml.ps1" -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
|
powershell.exe -NoProfile -File ".augment/skills/db-load-xml/scripts/db-load-xml.ps1" -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" <параметры>
|
powershell.exe -NoProfile -File ".augment/skills/db-run/scripts/db-run.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -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"
|
powershell.exe -NoProfile -File ".augment/skills/db-run/scripts/db-run.ps1" -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"
|
powershell.exe -NoProfile -File ".augment/skills/db-run/scripts/db-run.ps1" -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/Справочник.Номенклатура"
|
powershell.exe -NoProfile -File ".augment/skills/db-run/scripts/db-run.ps1" -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 "ЗапуститьОбновление"
|
powershell.exe -NoProfile -File ".augment/skills/db-run/scripts/db-run.ps1" -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" <параметры>
|
powershell.exe -NoProfile -File ".augment/skills/db-update/scripts/db-update.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -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"
|
powershell.exe -NoProfile -File ".augment/skills/db-update/scripts/db-update.ps1" -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 "+"
|
powershell.exe -NoProfile -File ".augment/skills/db-update/scripts/db-update.ps1" -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 "МоёРасширение"
|
powershell.exe -NoProfile -File ".augment/skills/db-update/scripts/db-update.ps1" -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" <параметры>
|
powershell.exe -NoProfile -File ".augment/skills/epf-build/scripts/epf-build.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -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"
|
powershell.exe -NoProfile -File ".augment/skills/epf-build/scripts/epf-build.ps1" -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"
|
powershell.exe -NoProfile -File ".augment/skills/epf-build/scripts/epf-build.ps1" -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" <параметры>
|
powershell.exe -NoProfile -File ".augment/skills/epf-dump/scripts/epf-dump.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -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"
|
powershell.exe -NoProfile -File ".augment/skills/epf-dump/scripts/epf-dump.ps1" -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"
|
powershell.exe -NoProfile -File ".augment/skills/epf-dump/scripts/epf-dump.ps1" -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>"]
|
powershell.exe -NoProfile -File ".augment/skills/epf-init/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-FormatVersion "<версия>"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Дальнейшие шаги
|
## Дальнейшие шаги
|
||||||
+27
-3
@@ -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/МояОбработка"
|
powershell.exe -NoProfile -File ".augment/skills/epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка"
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка/МояОбработка.xml"
|
powershell.exe -NoProfile -File ".augment/skills/epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка/МояОбработка.xml"
|
||||||
```
|
```
|
||||||
|
|
||||||
+23
-5
@@ -1,8 +1,9 @@
|
|||||||
# epf-validate v1.4 — Validate 1C external data processor / report structure
|
# epf-validate v1.6 — 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
|
||||||
|
[CmdletBinding(PositionalBinding=$false)]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory, Position=0)]
|
||||||
[Alias('Path')]
|
[Alias('Path')]
|
||||||
[string]$ObjectPath,
|
[string]$ObjectPath,
|
||||||
|
|
||||||
@@ -111,6 +112,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 +197,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.6 — 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" <параметры>
|
powershell.exe -NoProfile -File ".augment/skills/epf-build/scripts/epf-build.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -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"
|
powershell.exe -NoProfile -File ".augment/skills/epf-build/scripts/epf-build.ps1" -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"
|
powershell.exe -NoProfile -File ".augment/skills/epf-build/scripts/epf-build.ps1" -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" <параметры>
|
powershell.exe -NoProfile -File ".augment/skills/epf-dump/scripts/epf-dump.ps1" <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -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"
|
powershell.exe -NoProfile -File ".augment/skills/epf-dump/scripts/epf-dump.ps1" -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"
|
powershell.exe -NoProfile -File ".augment/skills/epf-dump/scripts/epf-dump.ps1" -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]
|
powershell.exe -NoProfile -File ".augment/skills/erf-init/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-FormatVersion "<версия>"] [-WithSKD]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Дальнейшие шаги
|
## Дальнейшие шаги
|
||||||
+27
-3
@@ -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/МойОтчёт"
|
powershell.exe -NoProfile -File ".augment/skills/epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт"
|
||||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
|
powershell.exe -NoProfile -File ".augment/skills/epf-validate/scripts/epf-validate.ps1" -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]
|
powershell.exe -NoProfile -File ".augment/skills/form-add/scripts/form-add.ps1" -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>"
|
powershell.exe -NoProfile -File ".augment/skills/form-compile/scripts/form-compile.ps1" -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>"
|
powershell.exe -NoProfile -File ".augment/skills/form-compile/scripts/form-compile.ps1" -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